/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.customization;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import nl.strohalm.cyclos.dao.customizations.CustomFieldDAO;
import nl.strohalm.cyclos.dao.customizations.CustomFieldPossibleValueDAO;
import nl.strohalm.cyclos.dao.customizations.CustomFieldValueDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.accounts.transactions.Transfer;
import nl.strohalm.cyclos.entities.customization.fields.CustomField;
import nl.strohalm.cyclos.entities.customization.fields.CustomField.Type;
import nl.strohalm.cyclos.entities.customization.fields.CustomFieldPossibleValue;
import nl.strohalm.cyclos.entities.customization.fields.CustomFieldValue;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue;
import nl.strohalm.cyclos.entities.customization.fields.Validation;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Element.Nature;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.services.elements.ElementServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.CustomFieldsContainer;
import nl.strohalm.cyclos.utils.CustomObjectHandler;
import nl.strohalm.cyclos.utils.ElementVO;
import nl.strohalm.cyclos.utils.Pair;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.RangeConstraint;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.StringHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.cache.Cache;
import nl.strohalm.cyclos.utils.cache.CacheCallback;
import nl.strohalm.cyclos.utils.cache.CacheManager;
import nl.strohalm.cyclos.utils.conversion.CalendarConverter;
import nl.strohalm.cyclos.utils.conversion.ConversionException;
import nl.strohalm.cyclos.utils.conversion.IdConverter;
import nl.strohalm.cyclos.utils.conversion.NumberConverter;
import nl.strohalm.cyclos.utils.lock.UniqueObjectHandler;
import nl.strohalm.cyclos.utils.validation.InvalidError;
import nl.strohalm.cyclos.utils.validation.LengthValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.UniqueError;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;
import nl.strohalm.cyclos.utils.validation.Validator.PropertyRetrieveStrategy;
import nl.strohalm.cyclos.webservices.model.FieldVO;
import nl.strohalm.cyclos.webservices.model.PossibleValueVO;
import nl.strohalm.cyclos.webservices.utils.FieldHelper;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.support.PropertyComparator;
/**
* Base implementation for custom field services
* @author luis
*/
public abstract class BaseCustomFieldServiceImpl<CF extends CustomField> implements BaseCustomFieldServiceLocal<CF> {
/**
* Validator for decimal fields
*
* @author luis
*/
public final class BigDecimalValidator implements PropertyValidation {
private static final long serialVersionUID = -7933981104151866154L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String str = (String) value;
final NumberConverter<BigDecimal> numberConverter = settingsService.getLocalSettings().getNumberConverter();
try {
numberConverter.valueOf(str);
return null;
} catch (final ConversionException e) {
return new InvalidError();
}
}
}
/**
* Retrieving strategy for validating properties
* @author luis
*/
public class CustomFieldRetrievingStrategy implements PropertyRetrieveStrategy {
private static final long serialVersionUID = 8667919404137289046L;
private final CustomField field;
public CustomFieldRetrievingStrategy(final CustomField field) {
this.field = field;
}
@Override
public Object description(final Object object, final String name) {
return field;
}
@Override
@SuppressWarnings("unchecked")
public Object get(final Object object) {
final Collection<? extends CustomFieldValue> values = (Collection<? extends CustomFieldValue>) PropertyHelper.get(object, "customValues");
final CustomFieldValue fieldValue = customFieldHelper.findByField(field, values);
String value = fieldValue == null ? null : fieldValue.getValue();
if (StringUtils.isNotEmpty(field.getPattern())) {
value = StringHelper.removeMask(field.getPattern(), value);
}
return value;
}
}
/**
* Validator for date fields
*
* @author luis
*/
public class DateValidator implements PropertyValidation {
private static final long serialVersionUID = 5145399976834903999L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String str = (String) value;
final CalendarConverter dateConverter = settingsService.getLocalSettings().getRawDateConverter();
try {
final Calendar date = dateConverter.valueOf(str);
if (date != null) {
final int year = date.get(Calendar.YEAR);
if (year < 1900 || year > 2100) {
return new InvalidError();
}
}
return null;
} catch (final ConversionException e) {
return new InvalidError();
}
}
}
/**
* Validator for enumerated fields
*
* @author luis
*/
public class EnumeratedValidator implements PropertyValidation {
private static final long serialVersionUID = 5145399976834903999L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String str = (String) value;
if (StringUtils.isEmpty(str)) {
return null;
}
final CustomField field = (CustomField) property;
CustomFieldPossibleValue possibleValue = null;
possibleValue = loadPossibleValue(str, field);
// Return error if not found
return possibleValue == null ? new InvalidError() : null;
}
}
/**
* Validator for integer fields
*
* @author luis
*/
public class IntegerValidator implements PropertyValidation {
private static final long serialVersionUID = 5145399976834903999L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String str = (String) value;
if (StringUtils.isNotEmpty(str) && !StringUtils.isNumeric(str)) {
return new InvalidError();
}
return null;
}
}
/**
* Validates a java identifier
* @author Jefferson Magno
*/
public class JavaIdentifierValidation implements PropertyValidation {
private static final long serialVersionUID = 259170291118675512L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String string = (String) value;
if (StringUtils.isNotEmpty(string) && !StringHelper.isValidJavaIdentifier(string)) {
return new InvalidError();
}
return null;
}
}
/**
* Validator for member fields
*
* @author luis
*/
public class MemberValidator implements PropertyValidation {
private static final long serialVersionUID = 5145399976834903999L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String idStr = (String) value;
if (StringUtils.isEmpty(idStr)) {
return null;
}
if (StringUtils.isNotEmpty(idStr)) {
Long id;
try {
id = Long.valueOf(idStr);
ElementVO elementVO = elementService.getElementVO(id);
if (elementVO.getNature() != Nature.MEMBER) {
throw new Exception();
}
} catch (Exception e) {
return new InvalidError();
}
}
return null;
}
}
/**
* Validates the parent field
* @author luis
*/
public final class ParentValidator implements PropertyValidation {
private static final long serialVersionUID = -6383825246336857857L;
@Override
@SuppressWarnings("unchecked")
public ValidationError validate(final Object object, final Object property, final Object value) {
final CF field = (CF) object;
final CustomField parent = (CustomField) value;
if (parent != null) {
final List<CF> possibleParents = listPossibleParentFields(field);
if (!possibleParents.contains(parent)) {
return new InvalidError();
}
}
return null;
}
}
/**
* Validate that an enum field value contains a possible value according to its parent value
* @author jcomas
*/
public class ParentValueValidation implements PropertyValidation {
private static final long serialVersionUID = 6222393116036296454L;
@Override
public ValidationError validate(final Object object, final Object data, final Object value) {
CustomFieldsContainer<?, ?> c;
if (!(object instanceof CustomFieldsContainer)) {
return null;
} else {
c = (CustomFieldsContainer<?, ?>) object;
}
final CustomField field = (CustomField) data;
CustomFieldValue customFieldValue = null;
CustomFieldPossibleValue possibleValue = null;
CustomFieldValue parentCustomFieldValue = null;
CustomFieldPossibleValue parentPossibleValue = null;
// Get the custom field values for actual field and its parent
for (CustomFieldValue cfv : c.getCustomValues()) {
if (customFieldValue == null && cfv.getField().getId() == field.getId()) {
customFieldValue = cfv;
} else if (parentCustomFieldValue == null && cfv.getField().getId() == field.getParent().getId()) {
parentCustomFieldValue = cfv;
}
if (customFieldValue != null && parentCustomFieldValue != null) {
break;
}
}
// If no customFieldValue, there is nothing to check
if (customFieldValue == null) {
return null;
}
// Get the possible values for actual and parent
possibleValue = loadPossibleValue(customFieldValue);
// If no possible value, there is nothing to check
if (possibleValue == null) {
return null;
}
parentPossibleValue = loadPossibleValue(parentCustomFieldValue);
if (parentPossibleValue == null || !parentPossibleValue.equals(possibleValue.getParent())) {
return new ValidationError("expected value " + possibleValue.getParent() + " in parent of field " + field);
}
return null;
}
}
/**
* A cache key for possible values. Cannot use the id itself to differentiate it from the field id
*
* @author luis
*/
public static class PossibleValueKey implements Serializable {
private static final long serialVersionUID = 6220627534414217532L;
private final long id;
public PossibleValueKey(final long id) {
this.id = id;
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof PossibleValueKey)) {
return false;
}
PossibleValueKey key = (PossibleValueKey) obj;
return id == key.id;
}
@Override
public int hashCode() {
return (int) id;
}
}
/**
* Validator to ensure the internal name is unique
*
* @author luis
*/
public class UniqueCustomFieldInternalNameValidation implements PropertyValidation {
private static final long serialVersionUID = 1L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final CustomField field = (CustomField) object;
if (field.getInternalName() == null || field.getInternalName().equals("")) {
return null;
}
return customFieldDao.isInternalNameUsed(field) ? new UniqueError(field.getInternalName()) : null;
}
}
/**
* Validates an unique field value
* @author luis
*/
public class UniqueFieldValueValidation implements PropertyValidation {
private static final long serialVersionUID = 6222393116036296454L;
@Override
public ValidationError validate(final Object object, final Object data, final Object value) {
if (!(object instanceof CustomFieldsContainer<?, ?>)) {
return null;
}
if (object instanceof Transfer && ((Transfer) object).getScheduledPayment() != null) {
// We cannot validate unique on scheduled payment installments, as all installments share the same values, and would always fail
return null;
}
final CustomField field = (CustomField) data;
final String string = (String) value;
if (StringUtils.isNotEmpty(string)) {
// Build a field value
CustomFieldValue fieldValue;
try {
fieldValue = field.getNature().getValueType().newInstance();
} catch (final Exception e) {
throw new RuntimeException(e);
}
fieldValue.setField(field);
fieldValue.setOwner(object);
fieldValue.setStringValue(string);
// Check uniqueness
if (customFieldValueDao.valueExists(fieldValue)) {
return new UniqueError(fieldValue.getStringValue());
}
}
return null;
}
}
private Validator possibleValueNavigator;
protected static final String ALL_KEY = "_ALL_";
protected static final List<String> EXCLUDED_PROPERTIES_FOR_DEPENDENT_FIELDS;
static {
final List<String> excluded = new ArrayList<String>();
excluded.add("class");
excluded.add("id");
excluded.add("name");
excluded.add("internalName");
excluded.add("parent");
excluded.add("description");
excluded.add("allSelectedLabel");
excluded.add("type");
excluded.add("control");
excluded.add("size");
excluded.add("description");
excluded.add("possibleValues");
excluded.add("children");
EXCLUDED_PROPERTIES_FOR_DEPENDENT_FIELDS = Collections.unmodifiableList(excluded);
}
protected final Class<CF> customFieldType;
protected FetchServiceLocal fetchService;
protected PermissionServiceLocal permissionService;
protected ElementServiceLocal elementService;
protected SettingsServiceLocal settingsService;
protected CustomFieldDAO customFieldDao;
protected CustomFieldValueDAO customFieldValueDao;
protected CustomFieldPossibleValueDAO customFieldPossibleValueDao;
private CacheManager cacheManager;
protected final Relationship[] fetch;
private Validator validator;
protected CustomObjectHandler customObjectHandler;
protected FieldHelper fieldHelper;
protected CustomFieldHelper customFieldHelper;
private UniqueObjectHandler uniqueObjectHandler;
protected BaseCustomFieldServiceImpl(final Class<CF> customFieldType) {
this.customFieldType = customFieldType;
Collection<Relationship> fetch = new ArrayList<Relationship>();
fetch.addAll(Arrays.asList(CustomField.Relationships.POSSIBLE_VALUES, CustomField.Relationships.CHILDREN, RelationshipHelper.nested(CustomField.Relationships.PARENT, CustomField.Relationships.POSSIBLE_VALUES)));
fetch.addAll(resolveAdditionalFetch());
this.fetch = fetch.toArray(new Relationship[fetch.size()]);
}
@Override
public void clearCache() {
getCache().clear();
}
@Override
public FieldVO getFieldVO(final Long customFieldId) {
if (customFieldId == null) {
return null;
}
CustomField cf = load(customFieldId);
return fieldHelper.toVO(cf);
}
@Override
public List<FieldVO> getFieldVOs(final List<Long> customFieldIds) {
if (customFieldIds == null) {
return null;
}
List<CustomField> customFields = new ArrayList<CustomField>(customFieldIds.size());
for (Long id : customFieldIds) {
if (id != null) {
CustomField cf = load(id);
customFields.add(cf);
}
}
return fieldHelper.toFieldVOs(customFields);
}
@Override
public List<PossibleValueVO> getPossibleValueVOs(final Long customFieldId, final Long possibleValueParentId) {
if (customFieldId == null) {
return null;
}
CF cf = load(customFieldId);
if (cf.getType() != CustomField.Type.ENUMERATED) {
return null;
}
FieldVO fieldVO = fieldHelper.toVO(cf);
List<PossibleValueVO> possibleValues = fieldVO.getPossibleValues();
if (possibleValues != null) {
if (possibleValueParentId != null) {
// Remove all the possible values that doesn't have parentValueId as parent
for (PossibleValueVO pvo : possibleValues) {
if (!pvo.getParentId().equals(possibleValueParentId)) {
possibleValues.remove(pvo);
}
}
}
return possibleValues;
} else {
return Collections.emptyList();
}
}
@Override
public List<CF> listPossibleParentFields(final CF field) {
if (field == null || (field.isPersistent() && field.getType() != CustomField.Type.ENUMERATED)) {
return new ArrayList<CF>();
}
final List<CF> fields = new ArrayList<CF>(list(field));
// Remove the field itself, those which are not enumerated and those who already have a parent (don't allow multiple levels)
for (final Iterator<CF> iterator = fields.iterator(); iterator.hasNext();) {
final CF current = iterator.next();
if (field.equals(current) || current.getType() != CustomField.Type.ENUMERATED || current.getControl() != CustomField.Control.SELECT || current.getParent() != null) {
iterator.remove();
}
}
return fields;
}
@Override
public List<CF> load(final Collection<Long> ids) {
List<CF> result = new ArrayList<CF>(ids.size());
for (Long id : ids) {
result.add(load(id));
}
return result;
}
@Override
public CF load(final Long id) {
return getCache().<CF> get(id, new CacheCallback() {
@Override
public Object retrieve() {
return loadChecked(id);
}
});
}
@Override
public CustomFieldPossibleValue loadPossibleValue(final Long id) {
return getCache().get("_POSSIBLE_VALUE_" + id, new CacheCallback() {
@Override
public Object retrieve() {
return loadCheckedPossibleValue(id);
}
});
}
@Override
@SuppressWarnings("unchecked")
public List<CustomFieldPossibleValue> loadPossibleValues(final Collection<Long> ids) {
List<CustomFieldPossibleValue> result = new ArrayList<CustomFieldPossibleValue>(ids.size());
for (Long id : ids) {
result.add(loadPossibleValue(id));
}
Collections.sort(result, new BeanComparator("value"));
return result;
}
@Override
public int remove(final Long... ids) {
for (Long id : ids) {
CustomField field = customFieldDao.load(id);
if (!customFieldType.isInstance(field)) {
throw new EntityNotFoundException();
}
}
getCache().clear();
return customFieldDao.delete(ids);
}
@Override
public int removePossibleValue(final Long... ids) {
for (Long id : ids) {
loadCheckedPossibleValue(id);
}
getCache().clear();
return customFieldPossibleValueDao.delete(ids);
}
@Override
public int replacePossibleValues(CustomFieldPossibleValue oldValue, CustomFieldPossibleValue newValue) {
oldValue = fetchService.fetch(oldValue);
newValue = fetchService.fetch(newValue);
if (!oldValue.getField().equals(newValue.getField())) {
throw new ValidationException();
}
return customFieldValueDao.moveValues(oldValue, newValue);
}
@Override
public CF save(CF field) {
// Special handling for fields with a parent field
CustomField parent = null;
if (field.getParent() != null) {
// When the field has a parent, several settings are copied from it
parent = fetchService.fetch(field.getParent());
copyParentProperties(parent, field);
}
validate(field);
if (field.isTransient()) {
field.setChildren(new ArrayList<CustomField>());
if (parent == null) {
int maxOrder = 0;
for (CF cf : list(field)) {
if (cf.getOrder() > maxOrder) {
maxOrder = cf.getOrder();
}
}
// Top level fields: set the order after other fields
field.setOrder(maxOrder + 1);
} else {
parent.getChildren().add(field);
}
// Save the field
field = customFieldDao.insert(field);
if (parent != null) {
// Nested fields: position the field just after his parent
final List<Long> order = new ArrayList<Long>();
List<CF> allFields = list(field);
for (int i = 0; i < allFields.size(); i++) {
CF cf = allFields.get(i);
if (cf.getParent() != null) {
continue;
}
order.add(cf.getId());
}
setOrder(order);
}
} else {
// Keep the order
final CustomField oldversion = customFieldDao.load(field.getId());
// in case of member custom fields, if set unhidden, all existing values must be unhidden
// TODO RINKE 1: TEST
if (oldversion instanceof MemberCustomField) {
final MemberCustomField oldversionMemberField = (MemberCustomField) oldversion;
final MemberCustomField newversion = (MemberCustomField) field;
if (oldversionMemberField.isMemberCanHide() && !newversion.isMemberCanHide()) {
// set all present field values to unhide
customFieldValueDao.unHideValues(newversion);
}
}
field.setOrder(oldversion.getOrder());
field = customFieldDao.update(field);
// Update the dependent properties for child fields
if (field.getType() == CustomField.Type.ENUMERATED) {
field = fetchService.reload(field, CustomField.Relationships.CHILDREN);
for (final CustomField child : field.getChildren()) {
copyParentProperties(field, child);
}
}
}
getCache().clear();
return field;
}
@Override
public CustomFieldPossibleValue save(CustomFieldPossibleValue possibleValue) throws ValidationException {
validate(possibleValue);
try {
if (possibleValue.isTransient()) {
possibleValue = customFieldPossibleValueDao.insert(possibleValue);
} else {
possibleValue = customFieldPossibleValueDao.update(possibleValue);
}
customFieldPossibleValueDao.ensureDefault(possibleValue);
} finally {
getCache().clear();
}
return possibleValue;
}
public final void setCacheManager(final CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public final void setCustomFieldDao(final CustomFieldDAO customFieldDao) {
this.customFieldDao = customFieldDao;
}
public void setCustomFieldHelper(final CustomFieldHelper customFieldHelper) {
this.customFieldHelper = customFieldHelper;
}
public final void setCustomFieldPossibleValueDao(final CustomFieldPossibleValueDAO customFieldPossibleValueDao) {
this.customFieldPossibleValueDao = customFieldPossibleValueDao;
}
public final void setCustomFieldValueDao(final CustomFieldValueDAO customFieldValueDao) {
this.customFieldValueDao = customFieldValueDao;
}
public final void setCustomObjectHandler(final CustomObjectHandler customObjectHandler) {
this.customObjectHandler = customObjectHandler;
}
public final void setElementServiceLocal(final ElementServiceLocal elementService) {
this.elementService = elementService;
}
public final void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public final void setFieldHelper(final FieldHelper fieldHelper) {
this.fieldHelper = fieldHelper;
}
@Override
@SuppressWarnings("unchecked")
public void setOrder(final List<Long> ids) {
int order = 0;
for (Long id : ids) {
CF field = loadChecked(id);
field.setOrder(++order);
List<CustomField> children = new ArrayList<CustomField>(field.getChildren());
if (CollectionUtils.isNotEmpty(children)) {
Collections.sort(children, new PropertyComparator("name", true, true));
for (CustomField child : children) {
child.setOrder(++order);
}
}
}
getCache().clear();
}
public final void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
this.permissionService = permissionService;
}
public final void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setUniqueObjectHandler(final UniqueObjectHandler uniqueObjectHandler) {
this.uniqueObjectHandler = uniqueObjectHandler;
}
@Override
public void validate(final CF field) {
getValidator().validate(field);
}
@Override
public void validate(final CustomFieldPossibleValue possibleValue) throws ValidationException {
getPossibleValueValidator().validate(possibleValue);
}
/**
* May be overridden in order to append any custom validations
*/
protected void appendValidations(final Validator validator) {
}
protected void doSaveValues(final CustomFieldsContainer<?, ?> owner) {
final Collection<? extends CustomFieldValue> customValues = owner.getCustomValues();
if (customValues == null || customValues.isEmpty()) {
return;
}
for (final CustomFieldValue value : customValues) {
// Retrieve the field value
final CustomField field = fetchService.fetch(value.getField());
lockUniqueFieldValue(field, value);
value.setField(field);
CustomFieldPossibleValue possibleValue = null;
Member memberValue = null;
String stringValue = null;
switch (field.getType()) {
case ENUMERATED:
// Load the possible value
Long possibleValueId = null;
if (value.getPossibleValue() != null) {
possibleValueId = value.getPossibleValue().getId();
} else {
possibleValueId = IdConverter.instance().valueOf(value.getValue());
}
boolean tryByValue = possibleValueId == null;
boolean invalidPossibleValue = false;
if (possibleValueId != null) {
// Try by id
try {
possibleValue = customFieldPossibleValueDao.load(possibleValueId);
invalidPossibleValue = !possibleValue.isEnabled();
} catch (final EntityNotFoundException e) {
tryByValue = true;
}
}
if (tryByValue && StringUtils.isNotEmpty(value.getValue())) {
// Try by field id + value
try {
possibleValue = customFieldPossibleValueDao.load(field.getId(), value.getValue());
invalidPossibleValue = !possibleValue.isEnabled();
} catch (final EntityNotFoundException e) {
invalidPossibleValue = true;
}
}
if (invalidPossibleValue) {
throw createValidationException(field);
}
break;
case MEMBER:
Long memberId = null;
if (value.getMemberValue() != null) {
memberId = value.getMemberValue().getId();
} else {
memberId = IdConverter.instance().valueOf(value.getValue());
}
if (memberId != null) {
boolean invalidMember = false;
try {
final Long mid = memberId;
memberValue = LoggedUser.runAsSystem(new Callable<Member>() {
@Override
public Member call() throws Exception {
Element element = elementService.load(mid);
if (!(element instanceof Member)) {
throw new EntityNotFoundException();
}
return (Member) element;
}
});
} catch (final EntityNotFoundException e) {
invalidMember = true;
}
if (invalidMember) {
throw createValidationException(field);
}
}
break;
default:
if ((field.getType() != CustomField.Type.STRING) || (field.getControl() != CustomField.Control.RICH_EDITOR)) {
stringValue = StringHelper.removeMarkupTags(value.getValue());
} else {
stringValue = value.getValue();
}
// A String value
stringValue = StringUtils.trimToNull(stringValue);
if (StringUtils.isNotEmpty(field.getPattern())) {
stringValue = StringHelper.removeMask(field.getPattern(), stringValue);
}
break;
}
// Check if the value exists for the given owner
try {
final CustomFieldValue existing = customFieldValueDao.load(field, owner);
// Exists - just update the value
existing.setStringValue(stringValue);
existing.setPossibleValue(possibleValue);
existing.setMemberValue(memberValue);
if (value instanceof MemberCustomFieldValue) {
((MemberCustomFieldValue) existing).setHidden(((MemberCustomFieldValue) value).isHidden());
}
customFieldValueDao.update(existing);
} catch (final EntityNotFoundException e) {
// Does not exists yet - insert a new value
value.setOwner(owner);
value.setStringValue(stringValue);
value.setPossibleValue(possibleValue);
value.setMemberValue(memberValue);
if (value.isTransient()) {
customFieldValueDao.insert(value);
} else {
customFieldValueDao.update(value);
}
}
}
}
protected Cache getCache() {
return cacheManager.getCache("cyclos.cache.CustomFields." + customFieldType.getSimpleName());
}
protected Validator getValueValidator(final Collection<? extends CustomField> fields) {
final Validator validator = new Validator();
for (CustomField field : fields) {
field = fetchService.fetch(field);
final Property property = validator.property(field.getInternalName(), new CustomFieldRetrievingStrategy(field));
property.displayName(field.getName());
switch (field.getType()) {
case BOOLEAN:
property.anyOf("true", "false");
break;
case INTEGER:
property.add(new IntegerValidator());
break;
case DATE:
property.add(new DateValidator());
break;
case ENUMERATED:
property.add(new EnumeratedValidator());
break;
case MEMBER:
property.add(new MemberValidator());
break;
case DECIMAL:
property.add(new BigDecimalValidator());
break;
case URL:
property.url(true);
break;
}
final Validation validation = field.getValidation();
if (validation != null) {
// Check required
boolean ignoreRequired = false;
if (field instanceof MemberCustomField) {
ServiceClient client = LoggedUser.serviceClient();
ignoreRequired = client != null && client.isIgnoreRegistrationValidations();
}
if (validation.isRequired() && !ignoreRequired) {
property.required();
}
// Check length constraint
final RangeConstraint lengthConstraint = validation.getLengthConstraint();
if (lengthConstraint != null) {
property.add(new LengthValidation(lengthConstraint));
}
// Check unique
if (validation.isUnique()) {
property.add(new UniqueFieldValueValidation());
}
// Custom validator class
if (StringUtils.isNotEmpty(validation.getValidatorClass())) {
final PropertyValidation validatorClass = customObjectHandler.get(validation.getValidatorClass());
property.add(validatorClass);
}
// Check that if enumerated, its value is consistent with the parent value
if (field.getType() == Type.ENUMERATED && field.getParent() != null) {
property.add(new ParentValueValidation());
}
}
}
return validator;
}
/**
* Should be implemented in order to list all fields like the given one. When fields are not dependent on other entities (like member / ads /
* admin / loan group) should return all. For others, like member record, should return all fields in the same member record as the given field
*/
protected abstract List<CF> list(CF field);
/**
* It locks the custom field value only if it's set as unique.
* @param value
*/
protected void lockUniqueFieldValue(final CustomField field, final CustomFieldValue value) {
if (field.getValidation().isUnique()) {
final Pair<Object, Object> pair = Pair.<Object, Object> of(field, value.getValue());
if (!uniqueObjectHandler.tryAcquire(pair)) {
throw new ValidationException(new UniqueError(field.getName()));
}
}
}
/**
* Must be implemented in order to resolve additional fetch to be applied before fields are stored on the cache
*/
protected Collection<? extends Relationship> resolveAdditionalFetch() {
return Collections.emptySet();
}
@SuppressWarnings("unchecked")
private void copyParentProperties(final CustomField parent, final CustomField child) {
final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(parent);
for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
final String name = propertyDescriptor.getName();
final boolean isWritable = propertyDescriptor.getWriteMethod() != null;
final boolean isReadable = propertyDescriptor.getReadMethod() != null;
if (isReadable && isWritable && !EXCLUDED_PROPERTIES_FOR_DEPENDENT_FIELDS.contains(name)) {
Object value = PropertyHelper.get(parent, name);
if (value instanceof Collection) {
value = new ArrayList<Object>((Collection<Object>) value);
}
PropertyHelper.set(child, name, value);
}
}
}
private ValidationException createValidationException(final CustomField field) {
final ValidationException vex = new ValidationException(field.getInternalName(), new InvalidError());
vex.setDisplayNameByProperty(Collections.singletonMap(field.getInternalName(), field.getName()));
return vex;
}
private Validator getPossibleValueValidator() {
if (possibleValueNavigator == null) {
final Validator validator = new Validator("customField.possibleValue");
validator.property("field").required();
validator.property("value").required().maxLength(255);
possibleValueNavigator = validator;
}
return possibleValueNavigator;
}
private Validator getValidator() {
if (validator == null) {
// We use a separate variable name to avoid concurrency problems with 2 threads modifying the same reference
Validator val = new Validator("customField");
val.property("internalName").required().maxLength(50).add(new JavaIdentifierValidation()).add(new UniqueCustomFieldInternalNameValidation());
val.property("name").required().maxLength(100);
val.property("type").required();
val.property("control").required();
val.property("size").required();
val.property("parent").add(new ParentValidator());
val.property("validation.validatorClass").instanceOf(PropertyValidation.class);
appendValidations(val);
validator = val;
}
return validator;
}
/**
* Loads a field, but only if it is of the expected type. Otherwise, throws an {@link EntityNotFoundException}
*/
private CF loadChecked(final Long id) {
CF field = customFieldDao.<CF> load(id, fetch);
if (!customFieldType.isInstance(field)) {
throw new EntityNotFoundException();
}
return field;
}
/**
* Loads a possible value, fetching both parent and field relationships, but throws an {@link EntityNotFoundException} if the field is not of the
* expected type
*/
private CustomFieldPossibleValue loadCheckedPossibleValue(final Long id) {
CustomFieldPossibleValue possibleValue = customFieldPossibleValueDao.load(id, CustomFieldPossibleValue.Relationships.PARENT, CustomFieldPossibleValue.Relationships.FIELD);
if (!customFieldType.isInstance(possibleValue.getField())) {
throw new EntityNotFoundException();
}
return possibleValue;
}
private CustomFieldPossibleValue loadPossibleValue(final CustomFieldValue v) {
if (v.getPossibleValue() != null) {
return v.getPossibleValue();
} else if (StringUtils.isEmpty(v.getValue())) {
return null;
} else {
return loadPossibleValue(v.getValue(), v.getField());
}
}
private CustomFieldPossibleValue loadPossibleValue(final String str, final CustomField field) {
CustomFieldPossibleValue possibleValue = null;
boolean byValue = true;
try {
if (StringUtils.isNumeric(str)) {
try {
possibleValue = customFieldPossibleValueDao.load(new Long(str));
if (field.equals(possibleValue.getField())) {
byValue = false;
}
} catch (final EntityNotFoundException e) {
// Not found - try by value
}
}
if (byValue) {
// Try by value
possibleValue = customFieldPossibleValueDao.load(field.getId(), str);
}
} catch (final EntityNotFoundException e) {
possibleValue = null;
}
return possibleValue;
}
}