/* 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.utils; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import nl.strohalm.cyclos.entities.customization.fields.AdCustomField; import nl.strohalm.cyclos.entities.customization.fields.AdminCustomField; 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.MemberCustomField.Access; import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.groups.AdminGroup; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.MemberGroup; import nl.strohalm.cyclos.entities.members.Element; import nl.strohalm.cyclos.entities.members.Member; import nl.strohalm.cyclos.entities.settings.LocalSettings; import nl.strohalm.cyclos.services.elements.ElementService; import nl.strohalm.cyclos.services.settings.SettingsService; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.conversion.CoercionHelper; import nl.strohalm.cyclos.utils.conversion.IdConverter; import nl.strohalm.cyclos.webservices.model.FieldValueVO; import nl.strohalm.cyclos.webservices.model.RegistrationFieldValueVO; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; /** * Helper class for custom field manipulation * @author luis */ public final class CustomFieldHelper { /** * Contains a relationship between custom field and its respective value * @author luis */ public class Entry implements Serializable { private static final long serialVersionUID = 1629234603383130863L; private final CustomField field; private final CustomFieldValue value; public Entry(final CustomField field, final CustomFieldValue value) { this.field = field; this.value = value; } public CustomField getField() { return field; } public CustomFieldValue getValue() { return value; } @Override public String toString() { String field, value; try { field = this.field.getName(); } catch (final NullPointerException e) { field = "null"; } try { value = this.value.getValue(); } catch (final NullPointerException e) { value = "null"; } return field + "=" + value; } } private SettingsService settingsService; private ElementService elementService; /** * Filters ad custom fields to be used on advanced search */ public List<AdCustomField> adFieldsForSearch(final List<AdCustomField> fields) { final List<AdCustomField> adFields = new ArrayList<AdCustomField>(); for (final AdCustomField field : fields) { if (field.isShowInSearch()) { adFields.add(field); } } return adFields; } /** * Builds a collection using all custom fields and their respective values, if any */ public Collection<Entry> buildEntries(final Collection<? extends CustomField> fields, final Collection<? extends CustomFieldValue> values) { if (fields == null) { return null; } final Collection<Entry> entries = new ArrayList<Entry>(fields.size()); for (final CustomField field : fields) { final CustomFieldValue fieldValue = findByField(field, values); if (fieldValue != null) { if (field.getType() == Type.MEMBER) { final Long id = IdConverter.instance().valueOf(fieldValue.getValue()); if (id != null) { fieldValue.setMemberValue(loadMember(id)); } } else if (StringUtils.isNotEmpty(field.getPattern())) { fieldValue.setValue(StringHelper.removeMask(field.getPattern(), fieldValue.getValue())); } } entries.add(new Entry(field, fieldValue)); } return entries; } /** * Builds a collection of field values given a value class, a collection of fields and a map of names/values */ public <V extends CustomFieldValue> Collection<V> buildValues(final Class<V> valueClass, final Collection<? extends CustomField> fields, final Map<String, String> values) { if (valueClass != null && fields != null && values != null) { final Collection<V> fieldValues = new ArrayList<V>(); for (final CustomField field : fields) { final String value = values.get(field.getInternalName()); final V fieldValue = ClassHelper.instantiate(valueClass); fieldValue.setField(field); if (StringUtils.isNotEmpty(value)) { final CustomField.Type type = field.getType(); if (type == CustomField.Type.ENUMERATED) { fieldValue.setPossibleValue(findPossibleValue(value, field.getPossibleValues(false))); } else if (type == CustomField.Type.MEMBER) { fieldValue.setMemberValue(loadMember(IdConverter.instance().valueOf(value))); } else { fieldValue.setStringValue(value); } } fieldValues.add(fieldValue); } return fieldValues; } return null; } /** * Clones the custom field values of a container and copy them to the other container setting the to container as the clone's owner */ public <CF extends CustomField, CFV extends CustomFieldValue> void cloneFieldValues(final CustomFieldsContainer<CF, CFV> from, final CustomFieldsContainer<CF, CFV> to) { cloneFieldValues(from, to, false); } /** * Clones the custom field values of a container and copy them to the other container * @param resetOwner if true sets the owner as null for each clone otherwise sets the "to container" as the owner. */ @SuppressWarnings("unchecked") public <CF extends CustomField, CFV extends CustomFieldValue> void cloneFieldValues(final CustomFieldsContainer<CF, CFV> from, final CustomFieldsContainer<CF, CFV> to, final boolean resetOwner) { final List<CFV> newCustomValues = new ArrayList<CFV>(); final Collection<CFV> customValues = from.getCustomValues(); if (customValues != null) { for (final CFV customValue : customValues) { final Object clone = customValue.clone(); final CFV newCustomValue = (CFV) clone; if (resetOwner) { newCustomValue.setOwner(null); } else { newCustomValue.setOwner(to); } newCustomValue.setId(null); newCustomValues.add(newCustomValue); } } to.setCustomValues(newCustomValues); } /** * Finds the value of the given field inside the collection */ public <V extends CustomFieldValue> V findByField(final CustomField field, final Collection<V> values) { if (values != null && field != null) { for (final V value : values) { if (field.equals(value.getField())) { return value; } } } return null; } /** * Finds the value of the given field inside the collection */ public <V extends CustomFieldValue> V findByFieldId(final Long fieldId, final Collection<V> values) { if (values != null && fieldId != null) { for (final V value : values) { if (value.getField().getId().equals(fieldId)) { return value; } } } return null; } /** * Finds the value of the given field inside the collection */ public <V extends CustomFieldValue> V findByFieldName(final String fieldName, final Collection<V> values) { if (values != null && StringUtils.isNotEmpty(fieldName)) { for (final V value : values) { if (value.getField().getInternalName().equals(fieldName)) { return value; } } } return null; } /** * Finds a custom field in a collection by it's identifier */ public <F extends CustomField> F findById(final Collection<F> fields, final Long id) { if (fields != null && id != null) { for (final F f : fields) { if (ObjectUtils.equals(f.getId(), id)) { return f; } } } return null; } /** * Attempts to find the field first by its id, then by its internal name. * @return */ public <F extends CustomField> F findByIdOrInternalName(final Collection<F> fields, final Long id, final String internalName) { F result = null; result = findById(fields, id); if (result == null) { result = findByInternalName(fields, internalName); } return result; } /** * Finds a custom field in a collection by it's internal name */ public <F extends CustomField> F findByInternalName(final Collection<F> fields, final String internalName) { if (fields != null && internalName != null) { for (final F f : fields) { if (ObjectUtils.equals(f.getInternalName(), internalName)) { return f; } } } return null; } /** * Finds a possible value reference on the collection */ public CustomFieldPossibleValue findPossibleValue(final String value, final Collection<CustomFieldPossibleValue> possibleValues) { if (StringUtils.isNotEmpty(value)) { for (final CustomFieldPossibleValue possibleValue : possibleValues) { if (value.equals(possibleValue.getValue())) { return possibleValue; } } } return null; } /** * Finds a possible value reference on the collection */ public CustomFieldPossibleValue findPossibleValueById(final Long id, final Collection<CustomFieldPossibleValue> possibleValues) { if (EntityHelper.isValidId(id) && CollectionUtils.isNotEmpty(possibleValues)) { for (final CustomFieldPossibleValue possibleValue : possibleValues) { if (id.equals(possibleValue.getId())) { return possibleValue; } } } return null; } /** * Finds a possible value label by id */ public String findPossibleValueById(final Object value, final Collection<CustomFieldPossibleValue> possibleValues) { long id; try { id = CoercionHelper.coerce(Long.TYPE, value); for (final CustomFieldPossibleValue possibleValue : possibleValues) { if (id == possibleValue.getId()) { return possibleValue.getValue(); } } } catch (final Exception e) { // Keep on } return null; } /** * Returns a Map keyed by the field internal name of field values as string */ public Map<String, String> getFields(final CustomFieldsContainer<?, ?> container) { final Map<String, String> values = new LinkedHashMap<String, String>(); for (final CustomFieldValue value : container.getCustomValues()) { values.put(value.getField().getInternalName(), value.getValue()); } return values; } /** * Returns the value of the field with the given internal name on the collection */ public <FV extends CustomFieldValue> FV getValue(final String internalName, final Collection<FV> customValues) { for (final FV value : customValues) { if (value.getField().getInternalName().equals(internalName)) { return value; } } return null; } /** * Returns a Map keyed by the custom field of custom field values */ @SuppressWarnings("unchecked") public <CF extends CustomField, CFV extends CustomFieldValue> Map<CF, CFV> getValuesByField(final CustomFieldsContainer<CF, CFV> container) { final Map<CF, CFV> values = new LinkedHashMap<CF, CFV>(); for (final CFV value : container.getCustomValues()) { values.put((CF) value.getField(), value); } return values; } /** * Returns a new collection with the result of merging the old custom field values and the new custom field values. The resulting field values * contains a subset of the allowed fields. * @param customFieldContainer the entity having the custom fields. * @param fieldValueVOs the new field values. * @param allowedFields the allowed fields that can be modified. */ public <T extends CustomFieldValue> Collection<T> mergeFieldValues(final CustomFieldsContainer<?, T> customFieldContainer, final List<? extends FieldValueVO> pFieldValueVOs, final List<? extends CustomField> allowedFields) { final Collection<T> currentFieldValues = new ArrayList<T>(customFieldContainer.getCustomValues()); // Clone the original list cause will make modifications. List<FieldValueVO> fieldValueVOs = null; if (pFieldValueVOs != null) { fieldValueVOs = new ArrayList<FieldValueVO>(); for (final FieldValueVO fv : pFieldValueVOs) { fieldValueVOs.add((FieldValueVO) fv.clone()); } // If the possible value or the value are not assigned then the value shouldn't be modified. for (final FieldValueVO fv : fieldValueVOs) { if (fv.getValue() == null && fv.getPossibleValueId() == null) { final CustomField cf = findByIdOrInternalName(allowedFields, fv.getFieldId(), fv.getInternalName()); if (cf != null) { final CustomFieldValue cfv = getValue(cf.getInternalName(), currentFieldValues); if (cfv != null) { fv.setValue(cfv.getValue()); if (cfv.getPossibleValue() != null) { fv.setPossibleValueId(cfv.getPossibleValue().getId()); } if (cfv.getMemberValue() != null) { fv.setMemberValueId(cfv.getMemberValue().getId()); } } } } if (fv instanceof RegistrationFieldValueVO) { final RegistrationFieldValueVO rfv = (RegistrationFieldValueVO) fv; if (rfv.getHidden() == null) { final CustomField cf = findByIdOrInternalName(allowedFields, rfv.getFieldId(), rfv.getInternalName()); if (cf != null) { final MemberCustomFieldValue cfv = (MemberCustomFieldValue) getValue(cf.getInternalName(), currentFieldValues); if (cfv != null) { rfv.setHidden(cfv.isHidden()); } } } } } } final Collection<T> newFieldValues = toValueCollection(allowedFields, fieldValueVOs); if (CollectionUtils.isEmpty(newFieldValues)) { return currentFieldValues; } // Add all the current values that weren't modified for (final T cv : currentFieldValues) { if (allowedFields.contains(cv.getField())) { final boolean modifiedFieldValue = getValue(cv.getField().getInternalName(), newFieldValues) != null; if (!modifiedFieldValue) { newFieldValues.add(cv); } } } return newFieldValues; } /** * Returns the basic fields only, that is, strings with control = text box, or integers or enums */ @SuppressWarnings("unchecked") public <T extends CustomField> List<T> onlyBasic(final List<T> customFields) { final List<T> result = new ArrayList<T>(customFields.size()); for (final CustomField field : customFields) { final CustomField.Type type = field.getType(); final CustomField.Control control = field.getControl(); boolean useField = false; if (type == CustomField.Type.STRING && control == CustomField.Control.TEXT) { useField = true; } else if (type == CustomField.Type.ENUMERATED || type == CustomField.Type.INTEGER) { useField = true; } if (useField) { result.add((T) field); } } return result; } /** * Filters the member custom field list, returning only those for ad search */ public List<MemberCustomField> onlyForAdSearch(final List<MemberCustomField> fields) { final List<MemberCustomField> memberFields = new ArrayList<MemberCustomField>(); final boolean unrestricted = LoggedUser.isSystemOrUnrestrictedClient(); final Group group = unrestricted ? null : LoggedUser.group(); for (final MemberCustomField field : fields) { final Access access = field.getAdSearchAccess(); if (unrestricted || access != null && access.granted(group, true, LoggedUser.isBroker(), false, LoggedUser.isWebService())) { memberFields.add(field); } } return memberFields; } /** * Filters the ad custom field list, returning only those for ad search */ public List<AdCustomField> onlyForAdsSearch(final List<AdCustomField> fields) { final Group.Nature nature = LoggedUser.hasUser() ? LoggedUser.group().getNature() : Group.Nature.MEMBER; final List<AdCustomField> adFields = new ArrayList<AdCustomField>(); for (final AdCustomField field : fields) { final AdCustomField.Visibility visibility = field.getVisibility(); if (visibility.granted(nature) || LoggedUser.isWebService() && visibility == AdCustomField.Visibility.WEB_SERVICE) { adFields.add(field); } } return adFields; } /** * Filters the admin custom field list, returning only those used for the given group */ public List<AdminCustomField> onlyForGroup(final List<AdminCustomField> fields, final AdminGroup group) { final List<AdminCustomField> adminFields = new ArrayList<AdminCustomField>(); for (final AdminCustomField field : fields) { if (field.getGroups().contains(group)) { adminFields.add(field); } } return adminFields; } /** * Filters the member custom field list, returning only those used for the given group */ public List<MemberCustomField> onlyForGroup(final List<MemberCustomField> fields, final MemberGroup group) { final List<MemberCustomField> memberFields = new ArrayList<MemberCustomField>(fields.size()); for (final MemberCustomField field : fields) { if (field.getGroups().contains(group)) { memberFields.add(field); } } return memberFields; } public List<MemberCustomField> onlyForGroups(final List<MemberCustomField> fields, final Collection<MemberGroup> groups) { final Set<MemberCustomField> memberFields = new HashSet<MemberCustomField>(); for (final MemberGroup group : groups) { memberFields.addAll(onlyForGroup(fields, group)); } return new ArrayList<MemberCustomField>(memberFields); } /** * Filters the member custom field list, returning only those for loan search */ public List<MemberCustomField> onlyForLoanSearch(final List<MemberCustomField> fields) { final List<MemberCustomField> memberFields = new ArrayList<MemberCustomField>(); final Group group = LoggedUser.group(); for (final MemberCustomField field : fields) { final Access access = field.getLoanSearchAccess(); if (access != null && access.granted(group, true, false, false, false)) { memberFields.add(field); } } return memberFields; } /** * Filters the member custom field list, returning only those for member search */ public List<MemberCustomField> onlyForMemberSearch(final List<MemberCustomField> fields) { final List<MemberCustomField> memberFields = new ArrayList<MemberCustomField>(fields.size()); if (LoggedUser.isSystemOrUnrestrictedClient()) { memberFields.addAll(fields); return memberFields; } final Group group = LoggedUser.group(); for (final MemberCustomField field : fields) { final Access access = field.getMemberSearchAccess(); if (access != null && access.granted(group, true, LoggedUser.isBroker(), false, LoggedUser.isWebService())) { memberFields.add(field); } } return memberFields; } public List<MemberCustomField> onlyInAllGroups(final List<MemberCustomField> fields, final Collection<MemberGroup> groups) { final Set<MemberCustomField> memberFields = new HashSet<MemberCustomField>(); for (final MemberCustomField f : fields) { if (f.getGroups().containsAll(groups)) { memberFields.add(f); } } return new ArrayList<MemberCustomField>(memberFields); } /** * Lists only fields which are owned by the given group */ public List<MemberCustomField> onlyOwnedFields(final List<MemberCustomField> fields, final MemberGroup group) { return doListVisibleFields(fields, group, true); } /** * Lists only fields which are visible by the given group */ public List<MemberCustomField> onlyVisibleFields(final List<MemberCustomField> fields, final MemberGroup group) { return doListVisibleFields(fields, group, false); } public void setElementService(final ElementService elementService) { this.elementService = elementService; } public void setSettingsService(final SettingsService settingsService) { this.settingsService = settingsService; } /** * Convert an array of FieldValue instances to a collection of CustomFieldValue */ @SuppressWarnings("unchecked") public <T extends CustomFieldValue> Collection<T> toValueCollection(final Collection<? extends CustomField> fields, final List<? extends FieldValueVO> fieldValues) { if (CollectionUtils.isEmpty(fields) || CollectionUtils.isEmpty(fieldValues)) { return Collections.emptySet(); } final List<T> customValues = new ArrayList<T>(); for (final FieldValueVO fieldValue : fieldValues) { final CustomField field = findByIdOrInternalName(fields, fieldValue.getFieldId(), fieldValue.getInternalName()); if (field == null) { throw new IllegalArgumentException("Couldn't find custom field for this field: " + fieldValue); } final T value = (T) ClassHelper.instantiate(field.getNature().getValueType()); value.setField(field); if (field.getType() == CustomField.Type.ENUMERATED) { if (EntityHelper.isValidId(fieldValue.getPossibleValueId())) { // Load the possible value and set its string representation. final CustomFieldPossibleValue possibleValue = findPossibleValueById(fieldValue.getPossibleValueId(), field.getPossibleValues(true)); if (possibleValue == null) { throw new IllegalArgumentException("Expected one of this values: " + field.getPossibleValues(true) + " for field: " + field); } value.setPossibleValue(possibleValue); } else { // Multiple values are allowed. However, we need to pass as ids to the inner layers, and expect here to get ids or names final Set<Long> possibleValueIds = new HashSet<Long>(); final String[] parts = StringUtils.split(fieldValue.getValue(), ','); for (String part : parts) { part = StringUtils.trimToNull(part); if (part == null) { continue; } CustomFieldPossibleValue possibleValue; if (EntityHelper.isValidId(fieldValue.getValue())) { possibleValue = findPossibleValueById(Long.parseLong(part), field.getPossibleValues(true)); } else { possibleValue = findPossibleValue(part, field.getPossibleValues(true)); } if (possibleValue == null) { throw new IllegalArgumentException("Expected one of this values: " + field.getPossibleValues(true) + " for field: " + field); } possibleValueIds.add(possibleValue.getId()); } value.setValue(StringUtils.join(possibleValueIds.iterator(), ',')); } } else if (field.getType() == CustomField.Type.MEMBER) { Member memberValue = null; final Long memberValueId = fieldValue.getMemberValueId(); boolean setMember = false; if (EntityHelper.isValidId(memberValueId)) { // Load the member memberValue = loadMember(memberValueId); setMember = true; } else if (!StringUtils.isEmpty(fieldValue.getValue())) { // Attempt by username memberValue = loadMember(fieldValue.getValue()); setMember = true; } if (setMember && memberValue == null) { throw new EntityNotFoundException(Member.class); } value.setMemberValue(memberValue); } else { // if it's a date value in ISO 8601 format, convert it to Cyclos date representation. Calendar parsedDateTime = null; if (field.getType() == CustomField.Type.DATE) { parsedDateTime = parseISO8601Date(fieldValue.getValue()); } if (parsedDateTime != null) { final LocalSettings localSettings = settingsService.getLocalSettings(); value.setValue(localSettings.getDateConverter().toString(parsedDateTime)); } else { value.setValue(fieldValue.getValue()); } if (StringUtils.isNotEmpty(field.getPattern())) { value.setValue(StringHelper.removeMask(field.getPattern(), value.getValue())); } } if (fieldValue instanceof RegistrationFieldValueVO && value instanceof MemberCustomFieldValue) { final RegistrationFieldValueVO reg = (RegistrationFieldValueVO) fieldValue; final MemberCustomFieldValue memberValue = (MemberCustomFieldValue) value; memberValue.setHidden(reg.getHidden() == null ? Boolean.FALSE : reg.getHidden()); } customValues.add(value); } return customValues; } private List<MemberCustomField> doListVisibleFields(List<MemberCustomField> fields, final MemberGroup group, final boolean byOwner) { fields = onlyForGroup(fields, group); for (final Iterator<MemberCustomField> iterator = fields.iterator(); iterator.hasNext();) { final MemberCustomField field = iterator.next(); if (!field.getVisibilityAccess().granted(group, byOwner, false, false, LoggedUser.isWebService())) { iterator.remove(); } } return fields; } private Member loadMember(final Long id) { try { return LoggedUser.runAsSystem(new Callable<Member>() { @Override public Member call() throws Exception { return elementService.<Member> load(id, Element.Relationships.GROUP); } }); } catch (final Exception e) { return null; } } private Member loadMember(final String username) { try { return (Member) elementService.loadUser(username).getElement(); } catch (final Exception e) { return null; } } private Calendar parseISO8601Date(final String date) { try { return javax.xml.bind.DatatypeConverter.parseDateTime(date); } catch (final IllegalArgumentException e) { return null; } } }