package com.adobe.acs.commons.wcm.impl; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.request.RequestParameter; import org.apache.sling.api.request.RequestParameterMap; import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.servlets.post.Modification; import org.apache.sling.servlets.post.SlingPostProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; /** * ACS AEM Commons - Property Merge Sling POST Processor */ @Component @Service public class PropertyMergePostProcessor implements SlingPostProcessor { private static final Logger log = LoggerFactory.getLogger(PropertyMergePostProcessor.class); private static final String AT_SUFFIX = "@PropertyMerge"; private static final String ALLOW_DUPLICATES_SUFFIX = AT_SUFFIX + ".AllowDuplicates"; private static final String TYPE_HINT_SUFFIX = AT_SUFFIX + ".TypeHint"; private static final String IGNORE_PREFIX = ":"; @Override public final void process(final SlingHttpServletRequest request, final List<Modification> modifications) throws Exception { final List<PropertyMerge> propertyMerges = this.getPropertyMerges(request.getRequestParameterMap()); final Resource resource = request.getResource(); for (final PropertyMerge propertyMerge : propertyMerges) { if (this.merge(resource, propertyMerge.getDestination(), propertyMerge.getSources(), propertyMerge.getTypeHint(), propertyMerge.isAllowDuplicates())) { modifications.add(Modification.onModified(resource.getPath())); log.debug("Merged property values from {} into [ {} ]", propertyMerge.getSources(), propertyMerge.getDestination()); } } } /** * Gets the corresponding list of PropertyMerge directives from the RequestParams. * * @param requestParameterMap the Request Param Map * @return a list of the PropertyMerge directives by Destination */ private List<PropertyMerge> getPropertyMerges(final RequestParameterMap requestParameterMap) { final HashMap<String, List<String>> mapping = new HashMap<String, List<String>>(); // Collect the Destination / Source mappings for (final RequestParameterMap.Entry<String, RequestParameter[]> entry : requestParameterMap.entrySet()) { if (!StringUtils.endsWith(entry.getKey(), AT_SUFFIX)) { // Not a @PropertyMerge request param continue; } final String source = StringUtils.removeStart(StringUtils.substringBefore(entry.getKey(), AT_SUFFIX), IGNORE_PREFIX); for (final RequestParameter requestParameter : entry.getValue()) { if (requestParameter != null) { final String destination = StringUtils.removeStart(StringUtils.stripToNull(requestParameter .getString()), IGNORE_PREFIX); if (destination != null) { List<String> sources = mapping.get(destination); if (sources == null) { sources = new ArrayList<String>(); } sources.add(source); mapping.put(StringUtils.strip(requestParameter.getString()), sources); } } } } // Convert the Mappings into PropertyMerge objects final List<PropertyMerge> propertyMerges = new ArrayList<PropertyMerge>(); for (final Map.Entry<String, List<String>> entry : mapping.entrySet()) { final String destination = entry.getKey(); final List<String> sources = entry.getValue(); RequestParameter allowDuplicatesParam = requestParameterMap.getValue(IGNORE_PREFIX + destination + ALLOW_DUPLICATES_SUFFIX); final boolean allowDuplicates = allowDuplicatesParam != null ? Boolean.valueOf(allowDuplicatesParam.getString()) : false; RequestParameter typeHintParam = requestParameterMap.getValue(IGNORE_PREFIX + destination + TYPE_HINT_SUFFIX); final String typeHint = typeHintParam != null ? typeHintParam.getString() : String.class.getSimpleName(); propertyMerges.add(new PropertyMerge(destination, sources, allowDuplicates, typeHint)); } return propertyMerges; } /** * Merges the values found in the the source properties into the destination property as a multi-value. * The values of the source properties and destination properties must all be the same property type. * * The unique set of properties will be stored in * * @param resource the resource to look for the source and destination properties on * @param destination the property to store the collected properties. * @param sources the properties to collect values from for merging * @param typeHint the data type that should be used when reading and storing the data * @param allowDuplicates true to allow duplicates values in the destination property; false to make values unique * @return true if changes were made to the destination property */ protected final <T> boolean merge(final Resource resource, final String destination, final List<String> sources, final Class<T> typeHint, final boolean allowDuplicates) throws PersistenceException { // Create an empty array of type T @SuppressWarnings("unchecked") final T[] emptyArray = (T[]) Array.newInstance(typeHint, 0); final ModifiableValueMap properties = resource.adaptTo(ModifiableValueMap.class); Collection<T> collectedValues = null; if (allowDuplicates) { collectedValues = new ArrayList<T>(); } else { collectedValues = new LinkedHashSet<T>(); } for (final String source : sources) { // Get the source value as type T final T[] tmp = properties.get(source, emptyArray); // If the value is not null, add to collectedValues if (tmp != null) { collectedValues.addAll(Arrays.asList(tmp)); } } final T[] currentValues = properties.get(destination, emptyArray); if (!collectedValues.equals(Arrays.asList(currentValues))) { properties.put(destination, collectedValues.toArray(emptyArray)); return true; } else { return false; } } /** * Encapsulates a PropertyMerge configuration by Destination. */ private static class PropertyMerge { private boolean allowDuplicates; private Class<?> typeHint; private String destination; private List<String> sources; public PropertyMerge(String destination, List<String> sources, boolean allowDuplicates, String typeHint) { this.destination = destination; this.sources = sources; this.allowDuplicates = allowDuplicates; this.typeHint = this.convertTypeHint(typeHint); } /** * Converts the String type hint to the corresponding class. * If not valid conversion can be found, default to String. * * @param typeHintStr the String representation of the type hint * @return the Class of the type hint */ private Class<?> convertTypeHint(final String typeHintStr) { if (Boolean.class.getSimpleName().equalsIgnoreCase(typeHintStr)) { return Boolean.class; } else if (Double.class.getSimpleName().equalsIgnoreCase(typeHintStr)) { return Double.class; } else if (Long.class.getSimpleName().equalsIgnoreCase(typeHintStr)) { return Long.class; } else if (Date.class.getSimpleName().equalsIgnoreCase(typeHintStr) || Calendar.class.getSimpleName().equalsIgnoreCase(typeHintStr)) { return Calendar.class; } else { return String.class; } } public boolean isAllowDuplicates() { return allowDuplicates; } public Class<?> getTypeHint() { return typeHint; } public String getDestination() { return destination; } public List<String> getSources() { return sources; } } }