/*
 * 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 extends K, ? extends V> 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();
    }
}