All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.granite.ui.components.BulkEditValueMap Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2015 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.granite.ui.components;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;

/**
 * BulkEditValueMap is a ValueMap specific to the needs of Bulk Editing; it is aimed at merging the given Resources'
 * ValueMaps. Please not that the merge is actually "virtual" since under the hood the ValueMap will always be empty;
 * in other words, get(Object key) performs an "on-demand" merge for the passed key.
 * In addition to providing the actual (merged) value for a given key, this specific ValueMap can also tell if a given
 * key has a mixed value using get(key + Field.IS_MIXED_SUFFIX).
 */
public class BulkEditValueMap implements ValueMap {

    private List resources;
    private HashMap cache; // Populated on-demand

    public BulkEditValueMap(List resources) {
        this.resources = resources;
        this.cache = new HashMap();
    }

    /**
     * Retrieves the merged value for the passed key. Calling get(key + Field.IS_MIXED_SUFFIX) returns true if the value
     * is mixed; false otherwise. If the value is non-existant null is returned.
     *
     * @param key
     *        The key of the value to retrieve.
     * @return The merged value for the passed key; or a boolean telling if the value is mixed or not (if the key ends
     * with Field.IS_MIXED_SUFFIX)
     */
    @Override
    public Object get(Object key) {
        String keyName = (String) key;
        MergedValue mergedValue = fetchMergedValue(keyName);
        return keyName.endsWith(Field.IS_MIXED_SUFFIX) ? mergedValue.isMixed() : mergedValue.getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T get(String name, Class type) {
        // takes into consideration Field.IS_MIXED_SUFFIX
        Object value = get(name);
        return type == null ? (T) value : convert(value, type);
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T get(String name, T defaultValue) {
        // takes into consideration Field.IS_MIXED_SUFFIX
        T value = get(name, defaultValue != null ? (Class) defaultValue.getClass() : null);
        return value == null ? defaultValue : value;
    }

    private MergedValue fetchMergedValue(String key) {
        MergedValue mergedValue;

        // Keys are stored without prefix, so we need to clean it first
        if (key.endsWith(Field.IS_MIXED_SUFFIX)) {
            key = key.replace(Field.IS_MIXED_SUFFIX, "");
        }

        // Check cache first
        if (this.cache.containsKey(key)) {
            mergedValue = this.cache.get(key);
        } else {
            List values = fetchValues(key);

            // All of the resources have no value for this key => value is common (but empty)
            if (values.size() == 0) {
                mergedValue = new MergedValue(null, false);
            }
            // At least one of the resources has no value for this key => value is mixed
            else if (values.size() < resources.size()) {
                mergedValue = new MergedValue(null, true);
            }
            else {
                mergedValue = new MergedValue(values);
            }

            // Store in cache
            this.cache.put(key, mergedValue);
        }
        return mergedValue;
    }

    // Returns all values found for the given key
    private List fetchValues(String key) {
        List values = new ArrayList();
        for (Resource resource : resources) {
            Object value = resource.getValueMap().get(key);
            if (value != null) {
                values.add(value);
            }
        }
        return values;
    }

    @Override
    public boolean equals(Object o) {
        return this == o;
    }

    // ---------- Unsupported "Enumeration" methods
    // Note that the underlying map gets populated on-demand (wherever a value for a given key is requested)

    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean containsKey(Object key) {
        return false;
    }

    @Override
    public boolean containsValue(Object value) {
        return false;
    }

    @Override
    public Set keySet() {
        return null;
    }

    @Override
    public Collection values() {
        return null;
    }

    @Override
    public Set> entrySet() {
        return null;
    }

    @Override
    public int hashCode() {
        return 0;
    }


    // ---------- Unsupported Modification methods

    @Override
    public void clear() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object put(String key, Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void putAll(Map t) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object remove(Object key) {
        throw new UnsupportedOperationException();
    }

    // ---------- Type conversion helper
    @SuppressWarnings("unchecked")
    private  T convert(Object obj, Class type) {
        try {
            if (obj == null) {
                return null;
            } else if (type.isAssignableFrom(obj.getClass())) {
                return (T) obj;
            } else if (type.isArray()) {
                return (T) convertToArray(obj, type.getComponentType());
            } else if (obj instanceof String && type == Calendar.class) {
               return (T) ISO8601.parse(obj.toString());
            } else if (obj instanceof Calendar && type == String.class) {
                return (T) ISO8601.format((Calendar) obj);
            } else if (type == String.class) {
                return (T) String.valueOf(obj);
            } else if (type == Integer.class) {
                return (T) (Integer) Integer.parseInt(obj.toString());
            } else if (type == Long.class) {
                return (T) (Long) Long.parseLong(obj.toString());
            } else if (type == Double.class) {
                return (T) (Double) Double.parseDouble(obj.toString());
            } else if (type == Boolean.class) {
                // We treat an empty string as "null" so that the default value is used
                return "".equals(obj.toString()) ? null : (T) (Boolean) Boolean.parseBoolean(obj.toString());
            } else {
                return null;
            }
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private  T[] convertToArray(Object obj, Class type) {
        List values = new LinkedList();
        if (obj.getClass().isArray()) {
            for (Object o : (Object[]) obj) {
                values.add(convert(o, type));
            }
        } else {
            values.add(convert(obj, type));
        }
        @SuppressWarnings("unchecked")
        T[] result = (T[]) Array.newInstance(type, values.size());
        return values.toArray(result);
    }

    // ---------- MergedValue inner class, contains the "merge" algorithm logic
    private class MergedValue {

        private Object value;
        private boolean isMixed;

        // Don't interfere with default values (null)
        private final static String MIXED_VALUE = "";

        public MergedValue(Object value, boolean isMixed) {
            this.value = value;
            this.isMixed = isMixed;
        }

        public MergedValue(List values) {
            // Assuming the rest of the values are Arrays too
            if (values.get(0).getClass().isArray()) {
                doMergeArrayValues(values);
            } else {
                doMergeSingleValues(values);
            }
        }

        private void doMergeSingleValues(List values) {
            Object mergedValue;
            boolean isMixed;

            // Keep only unique values
            Set set = new HashSet(values);
            List uniqueValues = new ArrayList(set);

            // All values could be merged to the same value
            if (uniqueValues.size() == 1) {
                isMixed = false;
                mergedValue = values.get(0);
            } else {
                isMixed = true;
                mergedValue = MIXED_VALUE;
            }

            this.isMixed = isMixed;
            this.value = mergedValue;
        }

        private boolean equalsIgnoreOrder(Object[] first, Object[] second) {
            HashSet firstSet = new HashSet(Arrays.asList(first));
            HashSet secondSet = new HashSet(Arrays.asList(second));
            return firstSet.equals(secondSet);
        }

        private void doMergeArrayValues(List arrayValues) {
            Object mergedValue;
            boolean isMixed = false;

            Object[] previousMerge = null;
            Object[] previousValue = null;

            // Compute intersection for each value
            for (Object arrayValue : arrayValues) {
                Object[] currentValue =  (Object[]) arrayValue;
                if (previousValue == null) {
                    previousValue = currentValue;
                    previousMerge = currentValue;
                } else {
                    Object[] currentMerge = CollectionUtils.intersection(
                            Arrays.asList(previousMerge),
                            Arrays.asList(currentValue)).toArray();

                    // As soon as the previous value differs from the current, it means values are mixed
                    if (!equalsIgnoreOrder(previousValue, currentValue)) {
                        isMixed = true;
                    }
                    previousValue = currentValue;
                    previousMerge = currentMerge;
                }
            }

            mergedValue = previousMerge;

            this.isMixed = isMixed;
            this.value = mergedValue;
        }

        public Object getValue() {
            return value;
        }

        public boolean isMixed() {
            return isMixed;
        }
    }
}