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

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

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*************************************************************************
* ADOBE CONFIDENTIAL
* ___________________
*
* Copyright 2015 Adobe
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe and its suppliers, if any. The intellectual
* and technical concepts contained herein are proprietary to Adobe
* and its suppliers and are protected by all applicable intellectual
* property laws, including trade secret and copyright laws.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe.
**************************************************************************/
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.Collections;
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 javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import com.adobe.granite.ui.components.impl.ValueMapValueFetchStrategy;
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, {@link #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 * {@code #get(key + Field.IS_MIXED_SUFFIX)}. */ public class BulkEditValueMap implements ValueMap { private List resources; private Map cache; private Map fetchStrategies; public BulkEditValueMap(@Nonnull List resources) { this(resources, Collections.emptyMap()); } public BulkEditValueMap(@Nonnull List resources, @Nonnull Map fetchStrategies) { this.resources = resources; this.cache = new HashMap(); this.fetchStrategies = fetchStrategies; } /** * Retrieves the merged value for the passed key. Calling {@code #get(key + * Field.IS_MIXED_SUFFIX)} returns {@code true} if the value is mixed; * {@code false} otherwise. If the value is non-existent {@code 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 * {@link Field#IS_MIXED_SUFFIX}). */ @Override public Object get(Object key) { String keyName = (String) key; @SuppressWarnings("null") MergedValue mergedValue = fetchMergedValue(keyName); return keyName.endsWith(Field.IS_MIXED_SUFFIX) ? mergedValue.isMixed() : mergedValue.getValue(); } @SuppressWarnings("unchecked") @Override public T get(@Nonnull String name, @Nonnull Class type) { // takes into consideration Field.IS_MIXED_SUFFIX Object value = get(name); return type == null ? (T) value : convert(value, type); } @Override @Nonnull public T get(@Nonnull String name, @Nonnull T defaultValue) { // takes into consideration Field.IS_MIXED_SUFFIX @SuppressWarnings("unchecked") T value = get(name, defaultValue != null ? (Class) defaultValue.getClass() : null); return value == null ? defaultValue : value; } @SuppressWarnings("null") @Nonnull private MergedValue fetchMergedValue(@Nonnull 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 { ValueFetchStrategy strategy = fetchStrategies.containsKey(key) ? fetchStrategies.get(key) : new ValueMapValueFetchStrategy(); List values = fetchValues(key, strategy); // 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 */ @Nonnull private List fetchValues(@Nonnull String key, @Nonnull ValueFetchStrategy fetchStrategy) { List values = new ArrayList(); for (Resource resource : resources) { Object value = fetchStrategy.apply(key, resource); if (value != null) { values.add(value); } } return values; } // ---------- 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; } // ---------- 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", "null" }) @CheckForNull private T convert(@CheckForNull Object obj, @Nonnull 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; } } @SuppressWarnings("null") @Nonnull private T[] convertToArray(@Nonnull Object obj, @Nonnull 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(@CheckForNull Object value, boolean isMixed) { this.value = value; this.isMixed = isMixed; } public MergedValue(@Nonnull 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(@Nonnull 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(@Nonnull Object[] first, @Nonnull Object[] second) { Set firstSet = new HashSet(Arrays.asList(first)); Set secondSet = new HashSet(Arrays.asList(second)); return firstSet.equals(secondSet); } @SuppressWarnings("null") private void doMergeArrayValues(@Nonnull 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; } @CheckForNull public Object getValue() { return value; } public boolean isMixed() { return isMixed; } } }