/* * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ package org.alfresco.util; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * A map that protects keys and values from accidental modification. *

* Use this map when keys or values need to be protected against client modification. * For example, when a component pulls a map from a common resource it can wrap * the map with this class to prevent any accidental modification of the shared * resource. *

* Upon first write to this map , the underlying map will be copied (selectively cloned), * the original map handle will be discarded and the copied map will be used. Note that * the map copy process will also occur if any mutable value is in danger of being * exposed to client modification. Therefore, methods that iterate and retrieve values * will also trigger the copy if any values are mutable. * * @param the map key type (must extend {@link Serializable}) * @param the map value type (must extend {@link Serializable}) * * @author Derek Hulley * @since 3.4.9 * @since 4.0.1 */ public class ValueProtectingMap implements Map, Serializable { private static final long serialVersionUID = -9073485393875357605L; /** * Default immutable classes: *

  • String
  • *
  • BigDecimal
  • *
  • BigInteger
  • *
  • Byte
  • *
  • Double
  • *
  • Float
  • *
  • Integer
  • *
  • Long
  • *
  • Short
  • *
  • Boolean
  • *
  • Date
  • *
  • Locale
  • */ public static final Set> DEFAULT_IMMUTABLE_CLASSES; static { DEFAULT_IMMUTABLE_CLASSES = new HashSet>(13); DEFAULT_IMMUTABLE_CLASSES.add(String.class); DEFAULT_IMMUTABLE_CLASSES.add(BigDecimal.class); DEFAULT_IMMUTABLE_CLASSES.add(BigInteger.class); DEFAULT_IMMUTABLE_CLASSES.add(Byte.class); DEFAULT_IMMUTABLE_CLASSES.add(Double.class); DEFAULT_IMMUTABLE_CLASSES.add(Float.class); DEFAULT_IMMUTABLE_CLASSES.add(Integer.class); DEFAULT_IMMUTABLE_CLASSES.add(Long.class); DEFAULT_IMMUTABLE_CLASSES.add(Short.class); DEFAULT_IMMUTABLE_CLASSES.add(Boolean.class); DEFAULT_IMMUTABLE_CLASSES.add(Date.class); DEFAULT_IMMUTABLE_CLASSES.add(Locale.class); } /** * Protect a specific value if it is considered mutable * * @param the type of the value, which must be {@link Serializable} * @param value the value to protect if it is mutable (may be null) * @param immutableClasses a set of classes that can be considered immutable * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} * @return a cloned instance (via serialization) or the instance itself, if immutable */ @SuppressWarnings("unchecked") public static S protectValue(S value, Set> immutableClasses) { if (!mustProtectValue(value, immutableClasses)) { return value; } // We have to clone it // No worries about the return type; it has to be the same as we put into the serializer return (S) SerializationUtils.deserialize(SerializationUtils.serialize(value)); } /** * Utility method to check if values need to be cloned or not * * @param the type of the value, which must be {@link Serializable} * @param value the value to check * @param immutableClasses a set of classes that can be considered immutable * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} * @return true if the value must NOT be given * to the calling clients */ public static boolean mustProtectValue(S value, Set> immutableClasses) { if (value == null) { return false; } Class clazz = value.getClass(); return ( DEFAULT_IMMUTABLE_CLASSES.contains(clazz) == false && immutableClasses.contains(clazz) == false); } /** * Utility method to clone a map, preserving immutable instances * * @param the map key type, which must be {@link Serializable} * @param the map value type, which must be {@link Serializable} * @param map the map to copy * @param immutableClasses a set of classes that can be considered immutable * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} */ public static Map cloneMap(Map map, Set> immutableClasses) { Map copy = new HashMap((int)(map.size() * 1.3)); for (Map.Entry element : map.entrySet()) { K key = element.getKey(); V value = element.getValue(); // Clone as necessary key = ValueProtectingMap.protectValue(key, immutableClasses); value = ValueProtectingMap.protectValue(value, immutableClasses); copy.put(key, value); } return copy; } private ReentrantReadWriteLock.ReadLock readLock; private ReentrantReadWriteLock.WriteLock writeLock; private boolean cloned = false; private Map map; private Set> immutableClasses; /** * Construct providing a protected map and using only the * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} * * @param protectedMap the map to safeguard */ public ValueProtectingMap(Map protectedMap) { this (protectedMap, null); } /** * Construct providing a protected map, complementing the set of * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} * * @param protectedMap the map to safeguard * @param immutableClasses additional immutable classes * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} * (may be null */ public ValueProtectingMap(Map protectedMap, Set> immutableClasses) { // Unwrap any internal maps if given a value protecting map if (protectedMap instanceof ValueProtectingMap) { ValueProtectingMap mapTemp = (ValueProtectingMap) protectedMap; this.map = mapTemp.map; } else { this.map = protectedMap; } this.cloned = false; if (immutableClasses == null) { this.immutableClasses = Collections.emptySet(); } else { this.immutableClasses = new HashSet>(immutableClasses); } // Construct locks ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); this.readLock = lock.readLock(); this.writeLock = lock.writeLock(); } /** * An unsafe method to use for anything except tests. * * @return the map that this instance is protecting */ /* protected */ Map getProtectedMap() { return map; } /** * Called by methods that need to force the map into a safe state. *

    * This method can be called without any locks being active. */ private void cloneMap() { readLock.lock(); try { // Check that it hasn't been copied already if (cloned) { return; } } finally { readLock.unlock(); } /* * Note: This space here is a window during which some code could have made * a copy. Therefore we will do a cautious double-check. */ // Put in a write lock before cloning the map writeLock.lock(); try { // Check that it hasn't been copied already if (cloned) { return; } Map copy = ValueProtectingMap.cloneMap(map, immutableClasses); // Discard the original this.map = copy; this.cloned = true; } finally { writeLock.unlock(); } } /* * READ-ONLY METHODS */ @Override public int size() { readLock.lock(); try { return map.size(); } finally { readLock.unlock(); } } @Override public boolean isEmpty() { readLock.lock(); try { return map.isEmpty(); } finally { readLock.unlock(); } } @Override public boolean containsKey(Object key) { readLock.lock(); try { return map.containsKey(key); } finally { readLock.unlock(); } } @Override public boolean containsValue(Object value) { readLock.lock(); try { return map.containsValue(value); } finally { readLock.unlock(); } } @Override public int hashCode() { readLock.lock(); try { return map.hashCode(); } finally { readLock.unlock(); } } @Override public boolean equals(Object obj) { readLock.lock(); try { return map.equals(obj); } finally { readLock.unlock(); } } @Override public String toString() { readLock.lock(); try { return map.toString(); } finally { readLock.unlock(); } } /* * METHODS THAT *MIGHT* REQUIRE COPY */ @Override public V get(Object key) { readLock.lock(); try { V value = map.get(key); return ValueProtectingMap.protectValue(value, immutableClasses); } finally { readLock.unlock(); } } /* * METHODS THAT REQUIRE COPY */ @Override public V put(K key, V value) { cloneMap(); return map.put(key, value); } @Override public V remove(Object key) { cloneMap(); return map.remove(key); } @Override public void putAll(Map m) { cloneMap(); map.putAll(m); } @Override public void clear() { cloneMap(); map.clear(); } @Override public Set keySet() { cloneMap(); return map.keySet(); } @Override public Collection values() { cloneMap(); return map.values(); } @Override public Set> entrySet() { cloneMap(); return map.entrySet(); } }