nl.strohalm.cyclos.services.customization.BaseCustomFieldServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.customization.BaseCustomFieldServiceImpl.java

Source

/*
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;
    }
}