You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by an...@apache.org on 2014/10/16 12:16:58 UTC

svn commit: r1632265 [2/5] - in /myfaces/trinidad/trunk: trinidad-api/src/test/java/org/apache/myfaces/trinidad/bean/util/ trinidad-api/src/test/java/org/apache/myfaces/trinidad/util/ trinidad-api/src/test/java/org/apache/myfaces/trinidadbuild/test/ tr...

Modified: myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/style/util/CSSUtils.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/style/util/CSSUtils.java?rev=1632265&r1=1632264&r2=1632265&view=diff
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/style/util/CSSUtils.java (original)
+++ myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/style/util/CSSUtils.java Thu Oct 16 10:16:54 2014
@@ -24,24 +24,22 @@ import java.net.URI;
 
 import java.net.URISyntaxException;
 
-import java.util.Collections;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.Vector;
+import java.util.concurrent.ConcurrentMap;
 
 import javax.faces.context.ExternalContext;
 import javax.faces.context.FacesContext;
 
 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
 import org.apache.myfaces.trinidad.util.ArrayMap;
-import org.apache.myfaces.trinidad.style.Style;
 import org.apache.myfaces.trinidadinternal.style.CSSStyle;
 import org.apache.myfaces.trinidadinternal.style.CoreStyle;
 import org.apache.myfaces.trinidadinternal.style.PropertyParseException;
-import org.apache.myfaces.trinidadinternal.util.LRUCache;
+import org.apache.myfaces.trinidadinternal.util.CopyOnWriteArrayMap;
 
 /**
  * CSS-related utilities. I think as we move away from xss, most of this code will
@@ -763,13 +761,13 @@ public class CSSUtils
   {
     Color sharedColor = _sColorCache.get(Integer.valueOf(rgb));
 
-    if (sharedColor == null)
-    {
-      sharedColor = new Color(rgb);
-      _sColorCache.put(Integer.valueOf(rgb), sharedColor);
-    }
+    if (sharedColor != null)
+      return sharedColor;
+
+    sharedColor = new Color(rgb);
+    Color existing = _sColorCache.putIfAbsent(Integer.valueOf(rgb), sharedColor);
 
-    return sharedColor;
+    return (existing != null) ? existing : sharedColor;
   }
 
   private static Color _getSharedColor(int r, int g, int b)
@@ -982,8 +980,7 @@ public class CSSUtils
     }
     
     return value.indexOf("url(") >= 0;
-  }  
-  
+  }
 
   private static final String _PARENTHESES_BEGIN = "(";
 
@@ -1007,8 +1004,7 @@ public class CSSUtils
   // We keep a cache of shared Color instances, hashed by RGB value, so
   // that we don't end up with one Color instance for each color in each
   // cache key in the Tecate image cache.
-  private static final Map<Integer, Color> _sColorCache = 
-    Collections.synchronizedMap(new LRUCache<Integer, Color>(50));
+  private static final ConcurrentMap<Integer, Color> _sColorCache = CopyOnWriteArrayMap.newLRUConcurrentMap(50);
 
   // CSS named color values
   private static final Object[] _NAMED_COLORS = new Object[]
@@ -1089,10 +1085,11 @@ public class CSSUtils
     "smaller",  10,
     "larger",   14
   };
-  
+
 
   // Set of values that are legal for url() values
   private static final Set<String> _URI_PROPERTIES = new HashSet<String>();
+
   static
   {
     _URI_PROPERTIES.add("background-image");
@@ -1103,12 +1100,13 @@ public class CSSUtils
 
   // Set of values that are legal for url() values
   private static final Set<String> _SPECIAL_URI_VALUES = new HashSet<String>();
+
   static
   {
     _SPECIAL_URI_VALUES.add("none");
     _SPECIAL_URI_VALUES.add("inherit");
     _SPECIAL_URI_VALUES.add("-moz-linear-gradient");
-    _SPECIAL_URI_VALUES.add("-webkit-gradient");    
+    _SPECIAL_URI_VALUES.add("-webkit-gradient");
     _SPECIAL_URI_VALUES.add("-webkit-linear-gradient");
     _SPECIAL_URI_VALUES.add("radial-gradient");
     _SPECIAL_URI_VALUES.add("linear-gradient");
@@ -1117,5 +1115,4 @@ public class CSSUtils
   }
 
   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(CSSUtils.class);
-
 }

Added: myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMap.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMap.java?rev=1632265&view=auto
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMap.java (added)
+++ myfaces/trinidad/trunk/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMap.java Thu Oct 16 10:16:54 2014
@@ -0,0 +1,1580 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.trinidadinternal.util;
+
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+
+import java.lang.reflect.Array;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import java.util.Set;
+import java.io.Serializable;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.myfaces.trinidad.util.Args;
+
+
+/**
+ * CopyOnWrite-implementation of ArrayMap
+ * Access is O(logN), using a binary search of the array of entries, sorted by key hash code.
+ * Null keys are not supported.
+ * Calling clone returns a new CopyOnWriteArrayMap with its own lock and its own set of
+ * Map.Entry objects.  The keys and values themselves are not cloned.
+ *
+ * The iterators returned run off a snapshot of the array of entry objects until remove() is called
+ * on the Iterator, If the set of entry objects has been modified since the Iterator was created,
+ * a ConcurrentModificationException will be thrown.  Otherwise, the remove() operation succeeds
+ * and the entry is removed from the CopyOnWriteArrayMap.
+ * @param <K>
+ * @param <V>
+ */
+public final class CopyOnWriteArrayMap<K,V> implements ConcurrentMap<K,V>, Cloneable, Serializable
+{
+  public static <K,V> CopyOnWriteArrayMap<K,V> newConcurrentMap()
+  {
+    ConcurrentEntryFactory<K,V> factory = ConcurrentEntryFactory.getInstance();
+
+    return new CopyOnWriteArrayMap<K,V>(factory);
+  }
+
+  /**
+   * Returns a CopyOnWriteArrayMap implementing a least-recently-used strategy to determine
+   * which entries to prune when the maxSize is exceeded.
+   *
+   * An entry is considered accessed if the entry's value is retreived or modified.
+   *
+   * The precision with which the least recently used entry is determined is dependent on the
+   * resolution of the platform's System.nanoTime() implementation.  In addition, no effort is
+   * made to handle entries that are updated while the CopyOnWriteArrayMap is determining
+   * which entry to purge.
+   *
+   * @param <K> Type of the keys in the CopyOnWriteArrayMap
+   * @param <V> Type of the mapped values
+   * @param maxSize The maximum number of entries allowed in the CopyOnWriteArrayMap
+   * @return
+   * @see java.lang.System#nanoTime
+   */
+  public static <K,V> CopyOnWriteArrayMap<K,V> newLRUConcurrentMap(int maxSize)
+  {
+    if (maxSize < 0)
+      maxSize = 0;
+
+    LRUEntryFactory<K,V> entryFactory = new LRUEntryFactory<K,V>(maxSize, System.nanoTime());
+
+    return new CopyOnWriteArrayMap<K,V>(entryFactory);
+  }
+
+  private CopyOnWriteArrayMap(EntryFactory<?,K,V> entryFactory)
+  {
+    this(entryFactory, new ReentrantLock(), entryFactory.getEmptyEntries());
+  }
+
+  private CopyOnWriteArrayMap(
+      EntryFactory<?,K,V> entryFactory, ReentrantLock writeLock, ConcurrentEntry<K,V>[] entries)
+  {
+    _entryFactory = entryFactory;
+    _writeLock = writeLock;
+    _entries   = entries;
+  }
+
+  @Override
+  public V putIfAbsent(K key, V value)
+  {
+    V oldValue;
+
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      ConcurrentEntry<K,V>[] entries = _entries;
+
+      int entryIndex = _getEntryIndex(entries, key);
+
+      if (entryIndex >= 0)
+      {
+        ConcurrentEntry<K,V> entry = entries[entryIndex];
+
+        oldValue = entry.getValue();
+      }
+      else
+      {
+        // the insert locations are returned as two's complement
+        int insertIndex = -(entryIndex + 1);
+
+        _entries = _insertEntryAt(entries, key, value, insertIndex, _entryFactory);
+
+        oldValue = null;
+      }
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+
+    return oldValue;
+  }
+
+  @Override
+  public boolean remove(Object key, Object value)
+  {
+    boolean removed = false;
+
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      ConcurrentEntry<K,V>[] entries = _entries;
+
+      int removeIndex = _getEntryIndex(entries, key);
+
+      if (removeIndex >= 0)
+      {
+        ConcurrentEntry<K,V> entry = entries[removeIndex];
+
+        V entryValue = entry.getValue();
+
+        boolean valuesEqual = (entryValue != null) ? entryValue.equals(value) : (value == null);
+
+        if (valuesEqual)
+        {
+          _entries = _removeEntryByIndex(entries, removeIndex);
+          removed = true;
+        }
+      }
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+
+    return removed;
+  }
+
+  @Override
+  public boolean replace(K key, V oldValue, V newValue)
+  {
+    ConcurrentEntry<K,V>[] entries = _entries;
+
+    ConcurrentEntry<K,V> entry = _getEntry(entries, key);
+
+    if (entry != null)
+    {
+      return entry.compareAndSetValue(oldValue, newValue);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  @Override
+  public V replace(K key, V value)
+  {
+    ConcurrentEntry<K,V>[] entries = _entries;
+
+    ConcurrentEntry<K,V> entry = _getEntry(entries, key);
+
+    if (entry != null)
+    {
+      return entry.setValue(value);
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  @Override
+  public int size()
+  {
+    return _entries.length;
+  }
+
+  @Override
+  public boolean isEmpty()
+  {
+    return _entries.length == 0;
+  }
+
+  @Override
+  public boolean containsKey(Object key)
+  {
+    return _getEntry(_entries, key) != null;
+  }
+
+  private static <K,V> ConcurrentEntry<K,V> _getEntry(ConcurrentEntry<K,V>[] entries, Object key)
+  {
+    if (key == null)
+      return null;
+
+    int entryIndex = _getEntryIndex(entries, key);
+
+    if (entryIndex >= 0)
+    {
+      return entries[entryIndex];
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  /**
+   * Handles the more complicated case where we matched the keyHashCode, but the key didn't match.
+   * This implies we have a hash collision and need to search the adjacent entries for a match
+   * @param entries
+   * @param key
+   * @param keyHashCode
+   * @param startIndex
+   * @return
+   */
+  private static <K,V> int _getHashCollsionMatchingEntryIndex(
+      ConcurrentEntry<K,V>[] entries, Object key, int keyHashCode, int startIndex)
+  {
+    int beforeIndex = startIndex - 1;
+
+    // search before the match
+    while (beforeIndex >= 0)
+    {
+      ConcurrentEntry<K,V> entry = entries[beforeIndex];
+
+      if (keyHashCode != entry.keyHashCode)
+        break;
+
+      if (key.equals(entry.getKey()))
+        return beforeIndex;
+
+      beforeIndex--;
+    }
+
+    // search after the match
+    int entryCount = entries.length;
+    int afterIndex = startIndex + 1;
+
+    while (afterIndex < entryCount)
+    {
+      ConcurrentEntry<K,V> entry = entries[afterIndex];
+
+      if (keyHashCode != entry.keyHashCode)
+        break;
+
+      if (key.equals(entry.getKey()))
+        return afterIndex;
+
+      afterIndex++;
+    }
+
+    // no match, but try ot optimize the index to be at the beginning or end of the array
+    int insertIndex;
+
+    if (beforeIndex == -1)
+      insertIndex = 0;
+    else
+      insertIndex = afterIndex;
+
+    // convert to two's complement
+    return -(insertIndex + 1);
+  }
+
+  /**
+   * Returns the positive index of the the entry, if the array contains an entry with the desired
+   * key.  If no entry is found, the desired insertion location is returned as the two's
+   * complement of the desired location.  For example, inserting before location 1, will be returned
+   * as -2.
+   * @param <K>
+   * @param <V>
+   * @param entries
+   * @param key non-null key to search for
+   * @return
+   */
+  private static <K,V> int _getEntryIndex(ConcurrentEntry<K,V>[] entries, Object key)
+  {
+    int keyHashCode = key.hashCode();
+
+    // find key using a binary search of the key hash codes
+    int lowIndex = 0;
+    int highIndex = entries.length - 1;
+
+    while (lowIndex <= highIndex)
+    {
+      int midIndex = (lowIndex + highIndex) >>> 1;
+
+      ConcurrentEntry<K,V> entry = entries[midIndex];
+
+      int midVal = entry.keyHashCode;
+
+      if (midVal < keyHashCode)
+      {
+        lowIndex = midIndex + 1;
+      }
+      else if (midVal > keyHashCode)
+      {
+        highIndex = midIndex - 1;
+      }
+      else
+      {
+        if (key.equals(entry.getKey()))
+        {
+          // found it
+          return midIndex;
+        }
+        else
+        {
+          // handle matching with hash collisions
+          return _getHashCollsionMatchingEntryIndex(entries, key, keyHashCode, midIndex);
+        }
+      }
+    }
+
+    // key not found, so returns two's complement of the index where we would insert
+    return -(lowIndex + 1);
+  }
+
+  private static <K,V> ConcurrentEntry<K,V>[] _removeEntryByIndex(
+      ConcurrentEntry<K,V>[] entries, int removeIndex)
+  {
+    int originalSize = entries.length;
+    int newSize = originalSize - 1;
+
+    @SuppressWarnings("unchecked")
+    ConcurrentEntry<K,V>[] newEntries = (ConcurrentEntry<K,V>[])
+        Array.newInstance(entries.getClass().getComponentType(), newSize);
+
+    if ((removeIndex == 0) || (removeIndex == newSize))
+    {
+      int srcStart = (removeIndex == 0) ? 1 : 0;
+
+      System.arraycopy(entries, srcStart, newEntries, 0, newSize);
+    }
+    else
+    {
+      // copy everything before the removeIndex
+      System.arraycopy(entries, 0, newEntries, 0, removeIndex);
+
+      // copy everything after the removeIndex, shifting down 1
+      System.arraycopy(entries, removeIndex + 1, newEntries, removeIndex, newSize - removeIndex);
+    }
+
+    return newEntries;
+  }
+
+  private static <K,V> ConcurrentEntry<K,V>[] _addEntryAtIndex(
+      ConcurrentEntry<K,V>[] entries, ConcurrentEntry<K,V> entry, int insertIndex, int removeIndex)
+  {
+    int originalSize = entries.length;
+    int newSize = originalSize;
+    
+    // if we haven't hit the LRU limit, increment the size
+    if (removeIndex < 0)
+      newSize++;
+
+    @SuppressWarnings("unchecked")
+    ConcurrentEntry<K,V>[] newEntries = (ConcurrentEntry<K,V>[])
+        Array.newInstance(entry.getClass(), newSize);
+
+    if (removeIndex >= 0)
+    {
+      if (removeIndex == insertIndex)
+      {
+        // inserting into same spot we removed, so just copy array
+        System.arraycopy(entries, 0, newEntries, 0, originalSize);
+      }
+      else
+      {
+        if (removeIndex < insertIndex)
+        {
+          // copy everything before the removeIndex
+          System.arraycopy(entries, 0, newEntries, 0, removeIndex);
+          
+          // copy everything between the removeIndex and the insertIndex, shifting things down
+          System.arraycopy(entries, removeIndex + 1, newEntries, removeIndex, insertIndex - removeIndex - 1);
+          
+          // copy everything from the entry index to the end
+          if (insertIndex < originalSize)
+          {
+            System.arraycopy(entries, insertIndex, newEntries, insertIndex, originalSize - insertIndex);
+          }
+
+          // we removed the entry before the insertion location, so decrement to account for this
+          insertIndex--;
+        }
+        else
+        {
+          // copy everything before the insertIndex
+          System.arraycopy(entries, 0, newEntries, 0, insertIndex);
+
+          // copy everything between the insertIndex and the removeIndex, shifting things up
+          System.arraycopy(entries, insertIndex, newEntries, insertIndex + 1, removeIndex - insertIndex);
+          
+          int afterRemoveIndex = removeIndex + 1;
+          
+          // copy everthing after the removeIndex
+          if (afterRemoveIndex < originalSize)
+          {
+            System.arraycopy(entries, afterRemoveIndex, newEntries, afterRemoveIndex, originalSize - afterRemoveIndex);
+          }
+        }
+      }
+    }
+    else
+    {
+      if ((insertIndex == 0) || (insertIndex == originalSize))
+      {
+        int destStart = (insertIndex == 0) ? 1 : 0;
+          
+        System.arraycopy(entries, 0, newEntries, destStart, originalSize);
+      }
+      else
+      {
+        // copy everything before the insertIndex
+        System.arraycopy(entries, 0, newEntries, 0, insertIndex);
+        
+        // copy everything after the insertIndex
+        System.arraycopy(entries, insertIndex, newEntries, insertIndex + 1, originalSize - insertIndex);
+      }
+    }
+
+    newEntries[insertIndex] = entry;
+    
+    return newEntries;
+  }
+
+  @Override
+  public boolean containsValue(Object value)
+  {
+    return _containsValue(_entries, value);
+  }
+
+  private static boolean _containsValue(ConcurrentEntry[] entries, Object value)
+  {
+    int entryCount = entries.length;
+
+    if (value == null)
+    {
+      for (int i = 0; i < entryCount; i++)
+      {
+        // don't touch the values to avoid messing up the LRU
+        if ( entries[i].getValueWithoutTouching() == null)
+        {
+          return true;
+        }
+      }
+    }
+    else
+    {
+      for (int i = 0; i < entryCount; i++)
+      {
+        // don't touch the values to avoid messing up the LRU
+        if (value.equals(entries[i].getValueWithoutTouching()))
+        {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  @Override
+  public V get(Object key)
+  {
+    ConcurrentEntry<K,V> entry = _getEntry(_entries, key);
+
+    if (entry != null)
+    {
+      return entry.getValue();
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  private static <K,V> ConcurrentEntry<K,V>[] _insertEntryAt(
+      ConcurrentEntry<K,V>[] entries, K key, V value, int insertIndex, EntryFactory<?,K,V> entryFactory)
+  {
+    ConcurrentEntry<K,V> entry = entryFactory.newEntry(key, value);
+
+    int removeIndex = entryFactory.getIndexOfEntryToPurge(entries);
+
+    return _addEntryAtIndex(entries, entry, insertIndex, removeIndex);
+  }
+
+  @Override
+  public V put(K key, V value)
+  {
+    V oldValue;
+
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      ConcurrentEntry<K,V>[] entries = _entries;
+
+      int entryIndex = _getEntryIndex(entries, key);
+
+      if (entryIndex >= 0)
+      {
+        ConcurrentEntry<K,V> entry = entries[entryIndex];
+
+        oldValue = entry.setValue(value);
+      }
+      else
+      {
+        // the insert locations are returned as two's complement
+        int insertIndex = -(entryIndex + 1);
+
+        _entries = _insertEntryAt(entries, key, value, insertIndex, _entryFactory);
+
+        oldValue = null;
+      }
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+
+    return oldValue;
+  }
+
+  @Override
+  public V remove(Object key)
+  {
+    V oldValue;
+
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      ConcurrentEntry<K,V>[] entries = _entries;
+
+      int removeIndex = _getEntryIndex(entries, key);
+
+      if (removeIndex >= 0)
+      {
+        ConcurrentEntry<K,V> entry = entries[removeIndex];
+
+        oldValue = entry.getValue();
+
+        _entries = _removeEntryByIndex(entries, removeIndex);
+      }
+      else
+      {
+        oldValue = null;
+      }
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+
+    return oldValue;
+  }
+
+  @Override
+  public void putAll(Map<? extends K, ? extends V> m)
+  {
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
+      {
+        put(e.getKey(), e.getValue());
+      }
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public void clear()
+  {
+    final ReentrantLock writeLock = this._writeLock;
+
+    writeLock.lock();
+
+    try
+    {
+      _entries = _entryFactory.getEmptyEntries();
+    }
+    finally
+    {
+      writeLock.unlock();
+    }
+  }
+
+  private final class KeySet extends AbstractSet<K>
+  {
+    @Override
+    public Iterator<K> iterator()
+    {
+      return new Iterator<K>()
+      {
+        private final Iterator<Entry<K,V>> _i = entrySet().iterator();
+
+        @Override
+        public boolean hasNext()
+        {
+          return _i.hasNext();
+        }
+
+        @Override
+        public K next()
+        {
+          return _i.next().getKey();
+        }
+
+        @Override
+        public void remove()
+        {
+          _i.remove();
+        }
+      };
+    }
+
+    @Override
+    public int size()
+    {
+      return CopyOnWriteArrayMap.this.size();
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+      return CopyOnWriteArrayMap.this.isEmpty();
+    }
+
+    @Override
+    public void clear()
+    {
+      CopyOnWriteArrayMap.this.clear();
+    }
+
+    @Override
+    public boolean contains(Object k)
+    {
+      return CopyOnWriteArrayMap.this.containsKey(k);
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, false);
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, true);
+    }
+  }
+
+  @Override
+  public Set<K> keySet()
+  {
+    return new KeySet();
+  }
+
+  private final class ValueCollection extends AbstractCollection<V>
+  {
+    public Iterator<V> iterator()
+    {
+      return new Iterator<V>()
+      {
+        private final Iterator<Entry<K,V>> _i = entrySet().iterator();
+
+        @Override
+        public boolean hasNext()
+        {
+          return _i.hasNext();
+        }
+
+        @Override
+        public V next()
+        {
+          ConcurrentEntry<K,V> entry = (ConcurrentEntry<K,V>)_i.next();
+
+          // don't touch the values when iterating
+          return entry.getValueWithoutTouching();
+        }
+
+        @Override
+        public void remove()
+        {
+          _i.remove();
+        }
+      };
+    }
+
+    @Override
+    public int size()
+    {
+      return CopyOnWriteArrayMap.this.size();
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+      return CopyOnWriteArrayMap.this.isEmpty();
+    }
+
+    @Override
+    public void clear()
+    {
+      CopyOnWriteArrayMap.this.clear();
+    }
+
+    @Override
+    public boolean contains(Object v)
+    {
+      return CopyOnWriteArrayMap.this.containsValue(v);
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, false);
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, true);
+    }
+  }
+
+  @Override
+  public Collection<V> values()
+  {
+    return new ValueCollection();
+  }
+
+  @Override
+  public Set<Entry<K, V>> entrySet()
+  {
+    return new EntrySet();
+  }
+
+  /**
+   * Either remove or retain all of the entries in the Collection c that are in ourIterable,
+   * grabbing a writeLock on the CopyOnWriteArrayMap before performing the operation
+   * @param <E>
+   * @param ourIterable
+   * @param c
+   * @param retain
+   * @return
+   */
+  boolean __removeOrRetainAll(Iterable<?> ourIterable, Collection<?> c, boolean retain)
+  {
+    boolean modified = false;
+
+    _writeLock.lock();
+
+    try
+    {
+      Iterator<?> it = ourIterable.iterator();
+
+      while (it.hasNext())
+      {
+        Object entry = it.next();
+
+        if (retain ^ c.contains(entry))
+        {
+          it.remove();
+          modified = true;
+        }
+      }
+    }
+    finally
+    {
+      _writeLock.unlock();
+    }
+
+    return modified;
+  }
+
+  private final class EntryIterator implements Iterator<Entry<K,V>>
+  {
+    EntryIterator()
+    {
+      _entries = CopyOnWriteArrayMap.this._entries;
+      _cursorIndex = 0;
+      _lastReturnedIndex = -1;
+    }
+
+    @Override
+    public boolean hasNext()
+    {
+      return _cursorIndex < _entries.length;
+    }
+
+    @Override
+    public Entry<K, V> next()
+    {
+      try
+      {
+        Entry<K,V> entry = _entries[_cursorIndex];
+        _lastReturnedIndex = _cursorIndex;
+        _cursorIndex++;
+
+        return entry;
+      }
+      catch (IndexOutOfBoundsException ioobe)
+      {
+        throw new NoSuchElementException();
+      }
+    }
+
+    @Override
+    public void remove()
+    {
+      // can't remove an entry we haven't visited or one that we already removed
+      if (_lastReturnedIndex < 0)
+        throw new IllegalStateException();
+
+      final ReentrantLock writeLock = CopyOnWriteArrayMap.this._writeLock;
+
+      writeLock.lock();
+
+      try
+      {
+        ConcurrentEntry<K,V>[] ourEntries = _entries;
+        ConcurrentEntry<K,V>[] baseEntries = CopyOnWriteArrayMap.this._entries;
+
+        // somebody has alread messed with the map since we were created
+        if (ourEntries != baseEntries)
+          throw new ConcurrentModificationException();
+
+        _entries = _removeEntryByIndex(ourEntries, _lastReturnedIndex);
+        CopyOnWriteArrayMap.this._entries = _entries;
+      }
+      finally
+      {
+        writeLock.unlock();
+      }
+
+      _cursorIndex--;
+
+      // don't allow double removals
+      _lastReturnedIndex = -1;
+    }
+
+    private ConcurrentEntry<K,V>[] _entries;
+    private int _cursorIndex;
+    private int _lastReturnedIndex;
+  }
+
+
+  private final class EntrySet extends AbstractSet<Entry<K,V>>
+  {
+    @Override
+    public Iterator<Entry<K,V>> iterator()
+    {
+      return new EntryIterator();
+    }
+
+    @Override
+    public boolean contains(Object o)
+    {
+      if (!(o instanceof Entry))
+        return false;
+
+      @SuppressWarnings("unchecked")
+      Entry<K,V> theirEntry = (Entry<K,V>)o;
+
+      ConcurrentEntry<K, V> ourEntry = _getEntry(_entries, theirEntry.getKey());
+
+      if (ourEntry != null)
+      {
+        // entries are equal if their keys and values are equal
+        Object ourValue = ourEntry.getValue();
+        Object theirValue = theirEntry.getValue();
+
+        return (ourValue != null) ? ourValue.equals(theirValue) : (theirValue == null);
+      }
+
+      return false;
+    }
+
+    @Override
+    public boolean remove(Object o)
+    {
+      if (!(o instanceof Entry))
+        return false;
+
+      @SuppressWarnings("unchecked")
+      Entry<K,V> removeEntry = (Entry<K,V>)o;
+
+      return CopyOnWriteArrayMap.this.remove(removeEntry.getKey(), removeEntry.getValue());
+    }
+
+    @Override
+    public int size()
+    {
+      return CopyOnWriteArrayMap.this.size();
+    }
+
+    @Override
+    public void clear()
+    {
+      CopyOnWriteArrayMap.this.clear();
+    }
+
+    @Override
+    public Object[] toArray()
+    {
+      // override for efficiency
+      ConcurrentEntry<K,V>[] entries = CopyOnWriteArrayMap.this._entries;
+      return Arrays.copyOf(entries, entries.length);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T[] toArray(T[] a)
+    {
+      // override for efficiency
+      ConcurrentEntry<K,V>[] entries = CopyOnWriteArrayMap.this._entries;
+      int entryCount = entries.length;
+
+      if (a.length < entryCount)
+      {
+        // destination array is too small, so return a new array of the correct type
+        return Arrays.copyOf(entries, entryCount, (Class<? extends T[]>) a.getClass());
+      }
+      else
+      {
+        System.arraycopy(entries, 0, a, 0, entryCount);
+
+        // add end marker, if the destination array is too big
+        if (a.length > entryCount)
+          a[entryCount] = null;
+
+        return a;
+      }
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, false);
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c)
+    {
+      return CopyOnWriteArrayMap.this.__removeOrRetainAll(this, c, true);
+    }
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (o == this)
+      return true;
+
+    if (!(o instanceof Map))
+      return false;
+
+    Map<?,?> otherMap = (Map<?,?>)o;
+
+    ConcurrentEntry<K,V>[] entries = _entries;
+    int entryCount = entries.length;
+
+    if (entryCount != otherMap.size())
+      return false;
+
+    for (int i = 0; i < entryCount; i++)
+    {
+      ConcurrentEntry<K,V> entry = entries[i];
+
+      K entryKey = entry.getKey();
+      V entryValue = entry.getValue();
+
+      if (entryValue != null)
+      {
+        Object otherValue = otherMap.get(entryKey);
+
+        if (!entryValue.equals(otherValue))
+        {
+          return false;
+        }
+      }
+      else
+      {
+        Object otherValue = otherMap.get(entryKey);
+
+        // use containsKey to handle case where the otherValue was null because the
+        // otherMap didn't contain an entry for this key
+        if ((otherValue != null) || !otherMap.containsKey(entryKey))
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+  @Override
+  public int hashCode()
+  {
+    int hash = 0;
+
+    ConcurrentEntry<K,V>[] entries = _entries;
+
+    for (Entry<K,V> entry : entries)
+    {
+      hash += entry.hashCode();
+    }
+
+    return hash;
+  }
+
+  @Override
+  public String toString()
+  {
+    ConcurrentEntry<K,V>[] entries = _entries;
+
+    // optimize the empty case
+    if (entries.length == 0)
+      return "{}";
+
+    StringBuilder sb = new StringBuilder();
+
+    sb.append('{');
+
+    boolean isFirstEntry = true;
+
+    for (Entry<K,V> entry : entries)
+    {
+      if (isFirstEntry)
+      {
+        isFirstEntry = false;
+      }
+      else
+      {
+        sb.append(", ");
+      }
+
+      K key   = entry.getKey();
+      V value = entry.getValue();
+
+      sb.append(key   == this ? "(this Map)" : key);
+      sb.append('=');
+      sb.append(value == this ? "(this Map)" : value);
+    }
+
+    sb.append('}');
+
+    return sb.toString();
+  }
+
+  /**
+   * Returns a shallow copy of this <tt>CopyOnWriteArrayMap</tt> instance with its own lock
+   * and entry objects: the keys and values themselves are not cloned.
+   *
+   * @return a shallow copy of this map
+   */
+  @Override
+  public CopyOnWriteArrayMap<K,V> clone()
+  {
+    ConcurrentEntry<K,V>[] entries = _entryFactory.cloneEntries(_entries);
+
+    // use a copy constructor since the writeLock is final, but the clone needs a new one
+    return new CopyOnWriteArrayMap<K,V>(_entryFactory, new ReentrantLock(), entries);
+  }
+
+  private void readObject(@SuppressWarnings("unused") ObjectInputStream inStream) throws InvalidObjectException
+  {
+    throw new InvalidObjectException("Proxy Required");
+  }
+
+  private Object writeReplace()
+  {
+    return _entryFactory.newSerializationProxy(_entries);
+  }
+
+  protected abstract static class SerializationProxy<E extends ConcurrentEntry<K,V>,K,V> implements Serializable
+  {
+    @SuppressWarnings("compatibility:-8476139976280416592")
+    private static final long serialVersionUID = 1L;
+
+    protected SerializationProxy(ConcurrentEntry<K,V>[] entries)
+    {
+      _keyValues = _createKeyValues(entries);
+    }
+
+    private Object readResolve()
+    {
+      ReentrantLock writeLock = new ReentrantLock();
+
+      EntryFactory<E,K,V> entryFactory = instantiateEntryFactory();
+      ConcurrentEntry<K,V>[] entries = instantiateEntries(_keyValues, entryFactory);
+
+      return new CopyOnWriteArrayMap<K,V>(entryFactory, writeLock, entries);
+    }
+
+    protected abstract E[] instantiateEntries(Object[] keyValues, EntryFactory<E,K,V> entryFactory);
+
+    protected abstract EntryFactory<E,K,V> instantiateEntryFactory();
+
+    private static <K,V> Serializable[] _createKeyValues(ConcurrentEntry<K,V>[] entries)
+    {
+      int entryCount = entries.length;
+
+      Serializable[] keyValues = new Serializable[entryCount *2];
+
+      for (int entryIndex = 0, keyValueIndex = 0; entryIndex < entryCount; entryIndex++)
+      {
+        ConcurrentEntry<K,V> entry = entries[entryIndex];
+        keyValues[keyValueIndex++] = (Serializable)entry.getKey();
+        keyValues[keyValueIndex++] = (Serializable)entry.getValue();
+      }
+
+      return keyValues;
+    }
+
+    private final Serializable[] _keyValues;
+  }
+
+  /**
+   * Manages the Entry-specific behavior between the ConcurrentEntries and the LRUEntries
+   */
+  private abstract static class EntryFactory<E extends ConcurrentEntry<K,V>, K, V>
+  {
+    /** Creates a new ConcurrentEntry with the specified key and value */
+    public abstract E newEntry(K key, V value);
+
+    /** Returns the index of an entry to remove as a result of adding another entry.  If
+     * a value < 0 is returned, no entry will be removed
+     * */
+    public abstract int getIndexOfEntryToPurge(E[] entries);
+
+    /** Returns a empty array of the correct type for this EntryFactory */
+    public abstract E[] getEmptyEntries();
+
+    /** Returns the Serialization proxy to use to serialize the CopyOnWriteArrayMap */
+    public abstract SerializationProxy<E,K,V> newSerializationProxy(E[] entries);
+
+    /**
+     * @param entries
+     * @return a deep copy of the ConcurrentEntry[].  The keys and values themselves are not cloned
+     */
+    public final E[] cloneEntries(E[] entries)
+    {
+      E[] clonedEntries = entries.clone();
+
+      int entryCount = entries.length;
+
+      for (int i = 0; i < entryCount; i++)
+      {
+        E originalEntry = clonedEntries[i];
+        E clonedEntry = newEntry(originalEntry.getKey(), originalEntry.getValue());
+
+        clonedEntries[i] = clonedEntry;
+      }
+
+      return clonedEntries;
+    }
+  }
+
+  private final static class ConcurrentEntryFactory<K,V> extends EntryFactory<ConcurrentEntry<K,V>,K,V>
+  {
+    @SuppressWarnings("unchecked")
+    public static <K,V> ConcurrentEntryFactory<K,V> getInstance()
+    {
+      return (ConcurrentEntryFactory<K,V>)_INSTANCE;
+    }
+
+    @Override
+    public ConcurrentEntry<K,V> newEntry(K key, V value)
+    {
+      return new ConcurrentEntry<K,V>(key, value);
+    }
+
+    @Override
+    public int getIndexOfEntryToPurge(ConcurrentEntry<K,V>[] entries)
+    {
+      // we never purge entries
+      return -1;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public ConcurrentEntry<K,V>[] getEmptyEntries()
+    {
+      return (ConcurrentEntry<K,V>[])_EMPTY_ENTRIES;
+    }
+
+    @Override
+    public SerializationProxy<ConcurrentEntry<K,V>,K,V> newSerializationProxy(ConcurrentEntry<K,V>[] entries)
+    {
+      return new ConcurrentSerializationProxy<K,V>(entries);
+    }
+
+    protected static final class ConcurrentSerializationProxy<K,V> extends SerializationProxy<ConcurrentEntry<K,V>, K,V>
+    {
+      @SuppressWarnings("compatibility:2346067066761682441")
+      private static final long serialVersionUID = 1L;
+
+      protected ConcurrentSerializationProxy(ConcurrentEntry<K,V>[] entries)
+      {
+        super(entries);
+      }
+
+      @Override
+      public EntryFactory<ConcurrentEntry<K,V>,K,V> instantiateEntryFactory()
+      {
+        return getInstance();
+      }
+
+      @Override
+      @SuppressWarnings({"cast", "unchecked"})
+      protected ConcurrentEntry<K,V>[] instantiateEntries(
+          Object[] keyValues, EntryFactory<ConcurrentEntry<K,V>,K,V> entryFactory)
+      {
+        int entryCount = keyValues.length / 2;
+
+        ConcurrentEntry<K,V>[] entries = (ConcurrentEntry<K,V>[])new ConcurrentEntry[entryCount];
+
+        for (int entryIndex = 0, keyValueIndex = 0; entryIndex < entryCount; entryIndex++)
+        {
+          K key   = (K)keyValues[keyValueIndex++];
+          V value = (V)keyValues[keyValueIndex++];
+
+          ConcurrentEntry<K,V> entry = new ConcurrentEntry<K,V>(key, value);
+          entries[entryIndex] = entry;
+        }
+
+        return entries;
+      }
+    }
+
+    private static final EntryFactory<?,?,?> _INSTANCE = new ConcurrentEntryFactory();
+    private static final ConcurrentEntry[] _EMPTY_ENTRIES = new ConcurrentEntry[0];
+  }
+
+  private final static class LRUEntryFactory<K,V> extends EntryFactory<LRUEntry<K,V>, K, V>
+  {
+    public LRUEntryFactory(int maxEntries, long baseNanos)
+    {
+      _maxEntries = maxEntries;
+      _baseNanos  = baseNanos;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public LRUEntry<K,V> newEntry(K key, V value)
+    {
+      return new LRUEntry<K,V>(key, value, this);
+    }
+
+    @Override
+    public int getIndexOfEntryToPurge(LRUEntry<K,V>[] entries)
+    {
+      if (_maxEntries <= entries.length)
+      {
+        // purge the oldest entry to make room for the new one
+        return _getOldestAccessesedEntryIndex(entries);
+      }
+      else
+      {
+        return -1;
+      }
+    }
+
+    /**
+     * @param entries
+     * @return The index of the least recently accessed entry
+     */
+    private static <K,V> int _getOldestAccessesedEntryIndex(LRUEntry<K,V>[] entries)
+    {
+      int entryCount = entries.length;
+      int oldestIndex = -1;
+      long oldestAccessedNanos = Long.MAX_VALUE;
+
+      for (int i = 0; i < entryCount; i++)
+      {
+        LRUEntry<K,V> entry = entries[i];
+        long currAccessedNanos = entry.getLastAccessed();
+
+        if (currAccessedNanos <= oldestAccessedNanos)
+        {
+          oldestIndex = i;
+          oldestAccessedNanos = currAccessedNanos;
+        }
+      }
+
+      return oldestIndex;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public LRUEntry<K,V>[] getEmptyEntries()
+    {
+      return (LRUEntry<K,V>[])_EMPTY_LRU_ENTRIES;
+    }
+
+    @Override
+    public SerializationProxy<LRUEntry<K,V>,K,V> newSerializationProxy(LRUEntry<K,V>[] entries)
+    {
+      return new LRUSerializationProxy<K,V>(_maxEntries, _baseNanos, entries);
+    }
+
+    /**
+     * @return delta in nano seconds from when the LRUConcurrentArrayMap was created.  If we exceed
+     * the magnitude of a long, return Long.MAX_VALUE.  In practice, this isn't a problem as
+     * it means that more than 292 years have elapsed.
+     */
+    public long nanosSinceCreated()
+    {
+      long nanos = System.nanoTime();
+      long delta = nanos - _baseNanos;
+
+      if (delta >= 0)
+      {
+        return delta;
+      }
+      else
+      {
+        // we have exceeded the magnitude of a long because the nano value wrapped around
+        return Long.MAX_VALUE;
+      }
+    }
+
+    protected static final class LRUSerializationProxy<K,V> extends SerializationProxy<LRUEntry<K,V>,K,V>
+    {
+      @SuppressWarnings("compatibility:-4809944737577473688")
+      private static final long serialVersionUID = 1L;
+
+      protected LRUSerializationProxy(int maxEntries, long baseNanos, ConcurrentEntry<K,V>[] entries)
+      {
+        super(entries);
+
+        _maxEntries = maxEntries;
+        _baseNanos  = baseNanos;
+      }
+
+      @Override
+      public EntryFactory<LRUEntry<K,V>, K, V> instantiateEntryFactory()
+      {
+        return new LRUEntryFactory<K,V>(_maxEntries, _baseNanos);
+      }
+
+      @Override
+      @SuppressWarnings({"cast", "unchecked"})
+      protected LRUEntry<K,V>[] instantiateEntries(
+          Object[] keyValues, EntryFactory<LRUEntry<K,V>,K,V> entryFactory)
+
+      {
+        int entryCount = keyValues.length / 2;
+
+        LRUEntry<K,V>[] entries = (LRUEntry<K,V>[])new LRUEntry[entryCount];
+
+        for (int entryIndex = 0, keyValueIndex = 0; entryIndex < entryCount; entryIndex++)
+        {
+          K key   = (K)keyValues[keyValueIndex++];
+          V value = (V)keyValues[keyValueIndex++];
+
+          LRUEntry<K,V> entry = new LRUEntry<K,V>(key, value, (LRUEntryFactory<K,V>)entryFactory);
+          entries[entryIndex] = entry;
+        }
+
+        return entries;
+      }
+
+      private final int _maxEntries;
+      private final long _baseNanos;
+    }
+
+    private static final ConcurrentEntry[] _EMPTY_LRU_ENTRIES = new LRUEntry[0];
+
+    private final int _maxEntries;
+    private final long _baseNanos;
+  }
+
+  protected static class ConcurrentEntry<K,V> implements Entry<K,V>
+  {
+    protected ConcurrentEntry(K key, V value)
+    {
+      Args.notNull(key, "key");
+
+      _key = key;
+      _value = value;
+      keyHashCode = key.hashCode();
+    }
+
+    @Override
+    public final K getKey()
+    {
+      return _key;
+    }
+
+    /** Returns the value without counting as accessing the entry */
+    public V getValueWithoutTouching()
+    {
+      return _value;
+    }
+
+    @Override
+    public V getValue()
+    {
+      return _value;
+    }
+
+    /**
+     * Version of setValue with compareAndSet semantics
+     * @param expected
+     * @param newValue
+     * @return
+     */
+    public boolean compareAndSetValue(V expected, V newValue)
+    {
+      return _VALUE_UPDATER.compareAndSet(this, expected, newValue);
+    }
+
+    @Override
+    @SuppressWarnings({"cast", "unchecked"})
+    public V setValue(V newValue)
+    {
+      return (V)_VALUE_UPDATER.getAndSet(this, newValue);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+      if (o == this)
+        return true;
+
+      if (o instanceof Entry)
+      {
+        Entry otherEntry = (Entry)o;
+
+        if (getKey().equals(otherEntry.getKey()))
+        {
+          Object otherValue = otherEntry.getValue();
+          V value = _value;
+
+          return (value != null) ? value.equals(otherValue) : otherValue == null;
+        }
+      }
+
+      return false;
+    }
+
+    @Override
+    public int hashCode()
+    {
+      V value = _value;
+      int valueHashCode = (value != null) ? value.hashCode() : 0;
+
+      return keyHashCode ^ valueHashCode;
+    }
+
+    @Override
+    public String toString()
+    {
+      return getKey() + "=" + _value;
+    }
+
+    private volatile V _value;
+    private final K _key;
+    public  final int keyHashCode;
+
+    // Apply AtomicReference love to the _value field
+    private static final AtomicReferenceFieldUpdater<ConcurrentEntry, Object> _VALUE_UPDATER =
+        AtomicReferenceFieldUpdater.newUpdater(ConcurrentEntry.class, Object.class, "_value");
+  }
+
+  private static final class LRUEntry<K,V> extends ConcurrentEntry<K,V>
+  {
+    public LRUEntry(K key, V value, LRUEntryFactory<K,V> nanoCalculator)
+    {
+      super(key, value);
+
+      _nanoCalculator = nanoCalculator;
+      _lastAccessed = _nanoCalculator.nanosSinceCreated();
+    }
+
+    public long getLastAccessed()
+    {
+      return _lastAccessed;
+    }
+
+    @Override
+    public V getValue()
+    {
+      // we don't especially care that we don't update the last accessed time atomically with
+      // updating the value
+      _lastAccessed = _nanoCalculator.nanosSinceCreated();
+
+      return super.getValue();
+    }
+
+    @Override
+    public V setValue(V newValue)
+    {
+      // we don't especially care that we don't update the last accessed time atomically with
+      // updating the value
+      _lastAccessed = _nanoCalculator.nanosSinceCreated();
+
+      return super.setValue(newValue);
+    }
+
+    private final LRUEntryFactory<K,V> _nanoCalculator;
+    private volatile long _lastAccessed;
+  }
+
+  @SuppressWarnings("compatibility:4274080938865508278")
+  private static final long serialVersionUID = 1;
+
+  private transient final EntryFactory<?,K,V> _entryFactory;
+
+  // lock protecting mutators
+  private transient final ReentrantLock _writeLock;
+
+  private volatile transient ConcurrentEntry<K,V>[] _entries;
+}
\ No newline at end of file

Modified: myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/RenderKitTestCase.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/RenderKitTestCase.java?rev=1632265&r1=1632264&r2=1632265&view=diff
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/RenderKitTestCase.java (original)
+++ myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/RenderKitTestCase.java Thu Oct 16 10:16:54 2014
@@ -228,14 +228,14 @@ abstract public class RenderKitTestCase 
 
     public BaseTest(String name,
                     String categoryName,
-                    String skin,
+                    String skinFamily,
                     Agent agent,
                     RequestContext.Accessibility accMode,
                     boolean rightToLeft,
                     String outputMode)
     {
       super(name + "-" + categoryName);
-      _skin = skin;
+      _skinFamily = skinFamily;
       _agent = agent;
       _accMode = accMode;
       _rightToLeft = rightToLeft;
@@ -274,7 +274,7 @@ abstract public class RenderKitTestCase 
     {
       _facesContext = createMockFacesContext(MApplication.sharedInstance(), true);
       _requestContext = createRequestContext();
-      _requestContext.setSkinFamily(_skin);
+      _requestContext.setSkinFamily(_skinFamily);
       _requestContext.setAgent(_agent);
       _requestContext.setRightToLeft(_rightToLeft);
       _requestContext.setOutputMode(_outputMode);
@@ -397,14 +397,13 @@ abstract public class RenderKitTestCase 
     private TestResult                   _result;
     private MFacesContext                _facesContext;
     private MRequestContext              _requestContext;
-    private String                       _skin;
+    private String                       _skinFamily;
     private String                       _outputMode;
     private Agent                        _agent;
     private RequestContext.Accessibility _accMode;
     private boolean                      _rightToLeft;
   }
 
-
   public class RendererTest extends BaseTest
   {
     public RendererTest(String name,
@@ -417,7 +416,6 @@ abstract public class RenderKitTestCase 
 
       _script = TestScriptParser.getTestScript(scriptFile, _facesConfigInfo);
       _lenient = lenient;
-      
 
       // We run golden-file checks on each subtest - though all differences
       // get counted only as a single diff.  We also do a comparison
@@ -475,12 +473,12 @@ abstract public class RenderKitTestCase 
       renderRoot(root);
 
       String golden = _loadGoldenFile();
-      
+
       String baseResults = base.toString();
-      
+
       _processResults(golden, baseResults);
     }
-  
+
     private String _loadGoldenFile() throws IOException
     {
       File goldenFile = new File(_goldenDir, getName() + "-golden.xml");
@@ -501,7 +499,7 @@ abstract public class RenderKitTestCase 
         golden = buffer.toString();
         in.close();
       }
-      
+
       return golden;
     }
 
@@ -510,19 +508,19 @@ abstract public class RenderKitTestCase 
       Iterator<TestScript.Test> tests = _script.getTests().iterator();
       Accessibility accMode = getAccMode();
       Agent agent = getAgent();
-      
+
       while (tests.hasNext())
       {
         TestScript.Test test = tests.next();
 
         boolean supportsAccessibilityMode = test.supportsAccessibilityMode(accMode);
-        boolean supportsAgent             = test.supportsAgent(agent);
-        boolean supportsLocale            = test.supportsLocale(RequestContext.getCurrentInstance());
-        
+        boolean supportsAgent = test.supportsAgent(agent);
+        boolean supportsLocale = test.supportsLocale(RequestContext.getCurrentInstance());
+
         if (supportsAccessibilityMode && supportsAgent && supportsLocale)
         {
           UIComponent testComponent = _createComponent();
-      
+
           test.apply(getFacesContext(), testComponent);
           docRoot.getChildren().add(new GatherContent(test.getOutput(),
                                                       testComponent,
@@ -530,7 +528,7 @@ abstract public class RenderKitTestCase 
                                                       this,
                                                       _lenient));
         }
-      }      
+      }
     }
 
     private void _processTest(TestScript.Test test, Writer out, String baseResults) throws IOException
@@ -539,8 +537,8 @@ abstract public class RenderKitTestCase 
       Agent agent = getAgent();
 
       boolean supportsAccessibilityMode = test.supportsAccessibilityMode(accMode);
-      boolean supportsAgent             = test.supportsAgent(agent);
-      boolean supportsLocale            = test.supportsLocale(RequestContext.getCurrentInstance());
+      boolean supportsAgent = test.supportsAgent(agent);
+      boolean supportsLocale = test.supportsLocale(RequestContext.getCurrentInstance());
 
       if (supportsAccessibilityMode && supportsAgent && supportsLocale)
       {
@@ -548,12 +546,12 @@ abstract public class RenderKitTestCase 
         out.write("\n<!--");
         out.write(test.toString());
         out.write("-->\n");
-        
+
         // surround content in a CDATA block to make it more likely to be valid XML
         //out.write("<![CDATA[\n");
-        
+
         String testResults;
-        
+
         try
         {
           testResults = test.getOutput().toString();
@@ -563,23 +561,22 @@ abstract public class RenderKitTestCase 
         {
           //out.write("\n]]>");         
         }
-      
+
         if (!_lenient)
         {
           if (!test.shouldMatchBase() &&
               baseResults.equals(testResults))
           {
             AssertionFailedError failure = new AssertionFailedError(
-              "In " + getName() + ", result of " + test.toString() + " were identical to " +
-              "base, but should not have been!");
+                "In " + getName() + ", result of " + test.toString() + " were identical to " +
+                "base, but should not have been!");
             getResult().addError(this, failure);
-          }
-          else if (test.shouldMatchBase() &&
+          } else if (test.shouldMatchBase() &&
                      !baseResults.equals(testResults))
           {
             AssertionFailedError failure = new AssertionFailedError(
-              "Result of " + test.toString() + " were not identical to " +
-              "base, but should have been!");
+                "Result of " + test.toString() + " were not identical to " +
+                "base, but should have been!");
             getResult().addError(this, failure);
           }
         }
@@ -613,17 +610,17 @@ abstract public class RenderKitTestCase 
         while (tests.hasNext())
         {
           TestScript.Test test = tests.next();
-          
+
           _processTest(test, out, baseResults);
         }
-        
-        out.write("\n</results>\n");    
+
+        out.write("\n</results>\n");
       }
       finally
       {
-        out.close();       
+        out.close();
       }
-      
+
       return out.toString();
     }
 
@@ -636,7 +633,7 @@ abstract public class RenderKitTestCase 
         failureFile = new File(_goldenDir, getName() + "-golden.xml");
       else
         failureFile = new File(_failureDir, getName() + "-golden.xml");
-          
+
       failureFile.getParentFile().mkdirs();
       FileWriter failureOut = new FileWriter(failureFile);
       failureOut.write(results);
@@ -654,8 +651,7 @@ abstract public class RenderKitTestCase 
           throw new AssertionFailedError("No golden file for test " +
                                          _scriptName);
         }
-      }
-      else
+      } else
       {
         int index = StringUtils.indexOfDifference(golden, results);
         String difference = StringUtils.difference(golden, results);
@@ -691,26 +687,26 @@ abstract public class RenderKitTestCase 
         }
         */
         throw new AssertionFailedError(
-             "Golden file for test "+ _scriptName + " did not match; " +
-             "first difference at " + index + ", difference of length " +
-             diffLength + ", \"" + difference + "\"");
-      }      
+            "Golden file for test " + _scriptName + " did not match; " +
+            "first difference at " + index + ", difference of length " +
+            diffLength + ", \"" + difference + "\"");
+      }
     }
-    
+
     private void _processResults(String golden, String baseResults) throws IOException
     {
       String results = _processTests(golden, baseResults);
-      
+
       if ((golden == null) || !golden.equals(results))
       {
         boolean forceGolden = "true".equals(
-                                    System.getProperty("org.apache.myfaces.trinidad.ForceGolden"));
-        
+            System.getProperty("org.apache.myfaces.trinidad.ForceGolden"));
+
         _writeFailureGoldenFile(results, forceGolden);
-        
+
         _throwAssertionFailure(golden, results, forceGolden);
       }
-    }      
+    }
 
     private UIComponent _createComponent()
     {
@@ -723,7 +719,6 @@ abstract public class RenderKitTestCase 
     private boolean    _lenient;
   }
 
-
   static private void _initGlobal() throws IOException, SAXException
   {
     RenderKitBootstrap bootstrap = new RenderKitBootstrap();
@@ -732,7 +727,7 @@ abstract public class RenderKitTestCase 
     _facesConfigInfo = bootstrap.getFacesConfigInfo();
 
     String projectPath = System.getProperty("user.dir");
-      
+
     String scripts = System.getProperty("trinidad.renderkit.scripts", projectPath + "/src/test/resources/org/apache/myfaces/trinidadinternal/renderkit/testScripts/");
     String golden = System.getProperty("trinidad.renderkit.golden", projectPath + "/src/test/resources/org/apache/myfaces/trinidadinternal/renderkit/golden/");
     String failures = System.getProperty("trinidad.renderkit.failures", projectPath + "/target/test-failures/");
@@ -835,24 +830,24 @@ abstract public class RenderKitTestCase 
   {
     public SuiteDefinition(
       String category,
-      String skin,
+      String skinFamily,
       RequestContext.Accessibility accessibilityMode,
       Agent  agent,
       boolean rightToLeft)
     {
-      this(category, skin, accessibilityMode, agent, rightToLeft, "default");
+      this(category, skinFamily, accessibilityMode, agent, rightToLeft, "default");
     }
 
     public SuiteDefinition(
       String category,
-      String skin,
+      String skinFamily,
       RequestContext.Accessibility accessibilityMode,
       Agent  agent,
       boolean rightToLeft,
       String outputMode)
     {
       _category = Args.notNull(category, "category");
-      _skin = Args.notNull(skin, "skin");
+      _skinFamily = Args.notNull(skinFamily, "skinFamily");
       _accessibilityMode = Args.notNull(accessibilityMode, "accessibilityMode");
       _agent = Args.notNull(agent, "agent");
       _rightToLeft = Args.notNull(rightToLeft, "rightToLeft");
@@ -864,10 +859,7 @@ abstract public class RenderKitTestCase 
       return _category;
     }
 
-    public String getSkin()
-    {
-      return _skin;
-    }
+    public String getSkin() { return _skinFamily; }
 
     public String getOutputMode()
     {
@@ -891,7 +883,7 @@ abstract public class RenderKitTestCase 
     }
 
     private String                       _category;
-    private String                       _skin;
+    private String                       _skinFamily;
     private String                       _outputMode;
     private RequestContext.Accessibility _accessibilityMode;
     private Agent                        _agent;
@@ -899,18 +891,17 @@ abstract public class RenderKitTestCase 
   }
 
   static private ExtendedRenderKitService _getExtendedRenderKitService(
-    FacesContext context)
+      FacesContext context)
   {
     return Service.getService(context.getRenderKit(),
                               ExtendedRenderKitService.class);
   }
 
-
   static private FacesConfigInfo _facesConfigInfo;
   static private File            _scriptDir;
   static private File            _goldenDir;
   static private File            _failureDir;
-  
+
   static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(RenderKitTestCase.class);
 
 }

Added: myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider1.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider1.java?rev=1632265&view=auto
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider1.java (added)
+++ myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider1.java Thu Oct 16 10:16:54 2014
@@ -0,0 +1,104 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.myfaces.trinidadinternal.skin.custom;
+
+import org.apache.myfaces.trinidad.skin.Skin;
+import org.apache.myfaces.trinidad.skin.SkinFactory;
+import org.apache.myfaces.trinidad.skin.SkinMetadata;
+import org.apache.myfaces.trinidad.skin.SkinProvider;
+import org.apache.myfaces.trinidad.skin.SkinVersion;
+
+import javax.faces.context.ExternalContext;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class CustomSkinProvider1 extends SkinProvider
+{
+  @Override
+  public Skin getSkin(ExternalContext context, SkinMetadata skinMetadata)
+  {
+    String skinId = skinMetadata.getId();
+    String family = skinMetadata.getFamily();
+    if ("purple.desktop".equals(skinId) || "purple".equals(family))
+    {
+      if (_purpleSkin == null)
+        _purpleSkin = SkinFactory.getFactory().createSkin(context, PURPLE);
+      return _purpleSkin;
+    }
+    if ("blue.desktop".equals(skinId)  || "blue".equals(family))
+    {
+      if (_blueSkin == null)
+        _blueSkin = SkinFactory.getFactory().createSkin(context, BLUE);
+      return _blueSkin;
+    }
+    if ("cyan.desktop".equals(skinId)  || "cyan".equals(family))
+    {
+      if (_cyanSkin == null)
+        _cyanSkin = SkinFactory.getFactory().createSkin(context, NO_CACHE_CYAN);
+      return _cyanSkin;
+    }
+    return null;
+  }
+
+  @Override
+  public Collection<SkinMetadata> getSkinMetadata(ExternalContext context)
+  {
+    return Collections.unmodifiableCollection(_METADATA);
+  }
+
+  public static final String PURPLE_SKIN_ID        = "purple.desktop";
+  public static final String NO_CACHE_CYAN_SKIN_ID = "cyan.desktop";
+  public static final String BLUE_SKIN_ID          = "blue.desktop";
+
+  private Skin _purpleSkin;
+  private Skin _blueSkin;
+  private Skin _cyanSkin;
+  private final static SkinMetadata      PURPLE        = new SkinMetadata.Builder()
+      .id(PURPLE_SKIN_ID)
+      .family("purple")
+      .version(new SkinVersion("v1"))
+      .baseSkinId("minimal.desktop")
+      .styleSheetName("org/apache/myfaces/trinidadinternal/skin/purple/purpleSkin.css")
+      .build();
+  private final static SkinMetadata      BLUE          = new SkinMetadata.Builder()
+      .id(BLUE_SKIN_ID)
+      .family("blue")
+      .version(new SkinVersion("v1"))
+      .baseSkinId(PURPLE_SKIN_ID)
+      .styleSheetName("org/apache/myfaces/trinidadinternal/skin/purple/purpleBigFontSkin.css")
+      .build();
+  private final static SkinMetadata      NO_CACHE_CYAN = new SkinMetadata.Builder()
+      .id(NO_CACHE_CYAN_SKIN_ID)
+      .family("cyan")
+      .version(new SkinVersion("v1"))
+      .baseSkinId("beach.desktop")
+      .styleSheetName("org/apache/myfaces/trinidadinternal/skin/purple/purpleBigFontSkinThree.css")
+      .build();
+  private final static Set<SkinMetadata> _METADATA     = new HashSet<SkinMetadata>();
+
+  static
+  {
+    _METADATA.add(PURPLE);
+    _METADATA.add(BLUE);
+    _METADATA.add(NO_CACHE_CYAN);
+  }
+
+}

Added: myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider2.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider2.java?rev=1632265&view=auto
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider2.java (added)
+++ myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/custom/CustomSkinProvider2.java Thu Oct 16 10:16:54 2014
@@ -0,0 +1,89 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.myfaces.trinidadinternal.skin.custom;
+
+import org.apache.myfaces.trinidad.skin.Skin;
+import org.apache.myfaces.trinidad.skin.SkinFactory;
+import org.apache.myfaces.trinidad.skin.SkinMetadata;
+import org.apache.myfaces.trinidad.skin.SkinProvider;
+import org.apache.myfaces.trinidad.skin.SkinVersion;
+
+import javax.faces.context.ExternalContext;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class CustomSkinProvider2 extends SkinProvider
+{
+  @Override
+  public Skin getSkin(ExternalContext context, SkinMetadata skinMetadata)
+  {
+    String skinId = skinMetadata.getId();
+    String family = skinMetadata.getFamily();
+
+    if ("violet.desktop".equals(skinId) || "violet".equals(family))
+    {
+      if (_violetSkin == null)
+        _violetSkin = SkinFactory.getFactory().createSkin(context, VIOLET);
+      return _violetSkin;
+    }
+    if ("green.desktop".equals(skinId) || "green".equals(family))
+    {
+      if (_greenSkin == null)
+        _greenSkin = SkinFactory.getFactory().createSkin(context, GREEN);
+      return _greenSkin;
+    }
+    return null;
+  }
+
+  @Override
+  public Collection<SkinMetadata> getSkinMetadata(ExternalContext context)
+  {
+    return Collections.unmodifiableCollection(_METADATA);
+  }
+
+  public static final String VIOLET_SKIN_ID = "violet.desktop";
+  public static final String GREEN_SKIN_ID = "green.desktop";
+
+  private Skin _violetSkin;
+  private Skin _greenSkin;
+  private final static SkinMetadata      VIOLET      = new SkinMetadata.Builder()
+      .id(VIOLET_SKIN_ID)
+      .family("violet")
+      .version(new SkinVersion("v1"))
+      .baseSkinId(CustomSkinProvider1.PURPLE_SKIN_ID)
+      .styleSheetName("org/apache/myfaces/trinidadinternal/skin/purple/purpleBigFontSkinTwo.css")
+      .build();
+  private final static SkinMetadata      GREEN       = new SkinMetadata.Builder()
+      .id(GREEN_SKIN_ID)
+      .family("green")
+      .version(new SkinVersion("v1"))
+      .baseSkinId("simple.desktop")
+      .styleSheetName("org/apache/myfaces/trinidadinternal/skin/purple/purpleBigFontSkinFour.css")
+      .build();
+  private final static Set<SkinMetadata> _METADATA   = new HashSet<SkinMetadata>();
+
+  static
+  {
+    _METADATA.add(VIOLET);
+    _METADATA.add(GREEN);
+  }
+
+}

Added: myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/provider/SkinProviderTest.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/provider/SkinProviderTest.java?rev=1632265&view=auto
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/provider/SkinProviderTest.java (added)
+++ myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/skin/provider/SkinProviderTest.java Thu Oct 16 10:16:54 2014
@@ -0,0 +1,452 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.myfaces.trinidadinternal.skin.provider;
+
+import junit.framework.TestCase;
+import org.apache.myfaces.trinidad.context.RequestContext;
+import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
+import org.apache.myfaces.trinidad.skin.Skin;
+import org.apache.myfaces.trinidad.skin.SkinFactory;
+import org.apache.myfaces.trinidad.skin.SkinMetadata;
+import org.apache.myfaces.trinidad.skin.SkinProvider;
+import org.apache.myfaces.trinidad.skin.SkinVersion;
+import org.apache.myfaces.trinidad.util.Service;
+import org.apache.myfaces.trinidadinternal.io.XhtmlResponseWriter;
+import org.apache.myfaces.trinidadinternal.renderkit.FacesConfigInfo;
+import org.apache.myfaces.trinidadinternal.renderkit.MApplication;
+import org.apache.myfaces.trinidadinternal.renderkit.MFacesContext;
+import org.apache.myfaces.trinidadinternal.renderkit.MRequestContext;
+import org.apache.myfaces.trinidadinternal.renderkit.NullWriter;
+import org.apache.myfaces.trinidadinternal.renderkit.RenderKitBootstrap;
+import org.apache.myfaces.trinidadinternal.renderkit.RenderUtils;
+import org.apache.myfaces.trinidadinternal.renderkit.TestResponseWriter;
+import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.TrinidadRenderingConstants;
+import org.apache.myfaces.trinidadinternal.skin.SkinFactoryImpl;
+import org.apache.myfaces.trinidadinternal.skin.custom.CustomSkinProvider1;
+import org.apache.myfaces.trinidadinternal.skin.custom.CustomSkinProvider2;
+
+import org.apache.myfaces.trinidad.component.core.CoreDocument;
+import org.apache.myfaces.trinidad.component.core.CoreForm;
+
+import javax.faces.component.UIViewRoot;
+import javax.faces.context.ExternalContext;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+public class SkinProviderTest extends TestCase
+{
+
+  public SkinProviderTest(String testName)
+  {
+    super(testName);
+  }
+
+  @Override
+  protected void setUp() throws IOException
+  {
+    MFacesContext ctx = new MFacesContext(MApplication.sharedInstance(), true);
+    ExternalContext externalContext = ctx.getExternalContext();
+
+    Map<String, Object> applicationMap = externalContext.getApplicationMap();
+    applicationMap.put(TrinidadSkinProvider.TRINDIAD_SKIN_PROVIDER_KEY,
+                       new TrinidadSkinProvider());
+    applicationMap.put(ExternalSkinProvider.EXTERNAL_SKIN_PROVIDER_KEY,
+                       new ExternalSkinProvider());
+    applicationMap.put(SkinProvider.SKIN_PROVIDER_INSTANCE_KEY,
+                       new SkinProviderRegistry());
+    _facesContext = ctx;
+    _externalContext = externalContext;
+    _provider = SkinProvider.getCurrentInstance(_externalContext);
+
+    if (SkinFactory.getFactory() == null)
+      SkinFactory.setFactory(new SkinFactoryImpl());
+    RenderKitBootstrap.setFactories(_facesConfigInfo);
+  }
+
+  @Override
+  protected void tearDown() throws IOException
+  {
+    MFacesContext.clearContext();
+    _facesContext = null;
+    _externalContext = null;
+    _provider = null;
+    RenderKitBootstrap.clearFactories();
+  }
+
+  public void testTrinidadBaseSkinsUsingId()
+  {
+    _testUsingId(TrinidadRenderingConstants.SIMPLE_DESKTOP_ID);
+    _testUsingId(TrinidadRenderingConstants.SIMPLE_PORTLET_ID);
+    _testUsingId(TrinidadRenderingConstants.SIMPLE_PDA_ID);
+    _testUsingId(TrinidadRenderingConstants.MINIMAL_DESKTOP_ID);
+    _testUsingId(TrinidadRenderingConstants.MINIMAL_PORTLET_ID);
+    _testUsingId(TrinidadRenderingConstants.MINIMAL_PDA_ID);
+    _testUsingId(TrinidadRenderingConstants.CASABLANCA_DESKTOP_ID);
+    _testUsingId(TrinidadRenderingConstants.CASABLANCA_PORTLET_ID);
+    _testUsingId(TrinidadRenderingConstants.CASABLANCA_PDA_ID);
+  }
+
+  public void testTrinidadBaseSkinsUsingFamily()
+  {
+    _testUsingFamily(TrinidadRenderingConstants.SIMPLE_SKIN_FAMILY,
+                     TrinidadRenderingConstants.SIMPLE_DESKTOP_ID);
+    _testUsingFamily(TrinidadRenderingConstants.MINIMAL_SKIN_FAMILY,
+                     TrinidadRenderingConstants.MINIMAL_DESKTOP_ID);
+    _testUsingFamily(TrinidadRenderingConstants.CASABLANCA_SKIN_FAMILY,
+                     TrinidadRenderingConstants.CASABLANCA_DESKTOP_ID);
+  }
+
+  public void testTrinidadBaseSkinsUsingFamilyAndRenderKit()
+  {
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.SIMPLE_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.DESKTOP.toString(),
+                                 TrinidadRenderingConstants.SIMPLE_DESKTOP_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.SIMPLE_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PDA.toString(),
+                                 TrinidadRenderingConstants.SIMPLE_PDA_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.SIMPLE_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PORTLET.toString(),
+                                 TrinidadRenderingConstants.SIMPLE_PORTLET_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.MINIMAL_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.DESKTOP.toString(),
+                                 TrinidadRenderingConstants.MINIMAL_DESKTOP_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.MINIMAL_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PDA.toString(),
+                                 TrinidadRenderingConstants.MINIMAL_PDA_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.MINIMAL_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PORTLET.toString(),
+                                 TrinidadRenderingConstants.MINIMAL_PORTLET_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.CASABLANCA_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.DESKTOP.toString(),
+                                 TrinidadRenderingConstants.CASABLANCA_DESKTOP_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.CASABLANCA_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PDA.toString(),
+                                 TrinidadRenderingConstants.CASABLANCA_PDA_ID);
+    _testUsingFamilyAndRenderkit(TrinidadRenderingConstants.CASABLANCA_SKIN_FAMILY,
+                                 SkinMetadata.RenderKitId.PORTLET.toString(),
+                                 TrinidadRenderingConstants.CASABLANCA_PORTLET_ID);
+  }
+
+  public void testNonExistingSkin()
+  {
+    SkinMetadata skinMetadata = new SkinMetadata.Builder().id("non-existing-skin").build();
+    Skin skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertTrue(TrinidadRenderingConstants.SIMPLE_DESKTOP_ID.equals(skin.getId()));
+
+    skinMetadata = new SkinMetadata.Builder().family("non-existing-family").build();
+    skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertTrue(TrinidadRenderingConstants.SIMPLE_DESKTOP_ID.equals(skin.getId()));
+
+    skinMetadata = new SkinMetadata.Builder().id("non-existing-skin")
+                                             .renderKitId(SkinMetadata.RenderKitId.PDA)
+                                             .build();
+    skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertTrue(TrinidadRenderingConstants.SIMPLE_PDA_ID.equals(skin.getId()));
+
+    skinMetadata = new SkinMetadata.Builder().family("non-existing-family")
+                                             .renderKitId(SkinMetadata.RenderKitId.PORTLET)
+                                             .build();
+    skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertTrue(TrinidadRenderingConstants.SIMPLE_PORTLET_ID.equals(skin.getId()));
+  }
+
+  public void testCircularSkin()
+  {
+    try
+    {
+      _testUsingId("circular1.desktop");
+      fail("Failed to detect circular dependency in trinidad-skins.xml");
+    }
+    catch (Exception e)
+    {
+      assertEquals("Unexpected type of exception caught.", "IllegalStateException", e.getClass().getSimpleName());
+    }
+  }
+
+  public void testXmlConfiguredSkins()
+  {
+    _testUsingId("empty.desktop");
+    _testUsingId("suede.desktop");
+    _testUsingId("beach.desktop");
+
+    _testUsingFamily("test", "empty.desktop");
+    _testUsingFamilyAndVersion("test", new SkinVersion(null, true), "empty.desktop");
+    _testUsingFamilyAndVersion("test", new SkinVersion("v1"), "empty.desktop");
+    _testUsingFamilyAndVersion("test", new SkinVersion("v2"), "suede.desktop");
+    _testUsingFamilyAndVersion("test", new SkinVersion("v3"), "beach.desktop");
+
+    SkinMetadata skinMetadata = new SkinMetadata.Builder().id("beach.desktop").build();
+    Skin beach = _provider.getSkin(_externalContext, skinMetadata);
+    assertEquals("Unexpected number of skin additions in beach.desktop.", 1, beach.getSkinAdditions().size());
+  }
+
+  public void testSkinMetadataFromProvider()
+  {
+    Collection<SkinProvider> providers = ((SkinProviderRegistry) _provider).getProviders();
+
+    assertEquals("Unexpected number of providers.", 5, providers.size());
+
+
+    for (SkinProvider provider : providers)
+    {
+      if (provider instanceof TrinidadBaseSkinProvider)
+        assertEquals("Unexpected number of skins in " + provider, 9, provider.getSkinMetadata(_externalContext).size());
+      else if (provider instanceof TrinidadSkinProvider)
+        assertEquals("Unexpected number of skins in " + provider, 5, provider.getSkinMetadata(_externalContext).size());
+      else if (provider instanceof ExternalSkinProvider)
+        assertEquals("Unexpected number of skins in " + provider, 0, provider.getSkinMetadata(_externalContext).size());
+      else if (provider instanceof CustomSkinProvider1)
+        assertEquals("Unexpected number of skins in " + provider, 3, provider.getSkinMetadata(_externalContext).size());
+      else if (provider instanceof CustomSkinProvider2)
+        assertEquals("Unexpected number of skins in " + provider, 2, provider.getSkinMetadata(_externalContext).size());
+    }
+
+    Collection<SkinMetadata> allSkins = _provider.getSkinMetadata(_externalContext);
+    assertEquals("Unexpected number of total skins.", 19, allSkins.size());
+  }
+
+  public void testCustomSkinProvider()
+  {
+    Skin greenSkin = _testUsingId("green.desktop");
+    Skin blueSkin = _testUsingId("blue.desktop");
+    Skin cyanSkin = _testUsingId("cyan.desktop");
+    Skin purpleSkin = _testUsingId("purple.desktop");
+    Skin violetSkin = _testUsingId("violet.desktop");
+    assertEquals("Unexpected base skin for green skin.", "simple.desktop", greenSkin.getBaseSkin().getId());
+    assertEquals("Unexpected base skin for blue skin.", "purple.desktop", blueSkin.getBaseSkin().getId());
+    assertEquals("Unexpected base skin for cyan skin.", "beach.desktop", cyanSkin.getBaseSkin().getId());
+    assertEquals("Unexpected base skin for cyan skin.", "minimal.desktop", purpleSkin.getBaseSkin().getId());
+    assertEquals("Unexpected base skin for cyan skin.", "purple.desktop", violetSkin.getBaseSkin().getId());
+    assertEquals("Unexpected purple base skin in violet skin.", purpleSkin, violetSkin.getBaseSkin());
+  }
+
+  public void testSkinCache()
+  {
+
+    _cleanTempDir();
+
+    // access skin from external provider
+    _initRequestContextAndGenerateCss("green");
+    _verifyNumberOfSkinInCache(0);
+    _verifyNumberOfCssFiles(1);
+    _releaseRequestContext();
+
+    // access skin from external provider
+    _initRequestContextAndGenerateCss("blue");
+    _verifyNumberOfSkinInCache(0);
+    _verifyNumberOfCssFiles(2);
+    _releaseRequestContext();
+
+    // access internal skin defined in trinidad-skins.xml
+    _initRequestContextAndGenerateCss("suede");
+    _verifyNumberOfSkinInCache(1);
+    _verifyNumberOfCssFiles(3);
+    _releaseRequestContext();
+
+    // access skin from external provider
+    _initRequestContextAndGenerateCss("cyan");
+    _verifyNumberOfSkinInCache(1);
+    _verifyNumberOfCssFiles(4);
+    _releaseRequestContext();
+
+    // access internal static skin
+    _initRequestContextAndGenerateCss("casablanca");
+    _verifyNumberOfSkinInCache(2);
+    _verifyNumberOfCssFiles(5);
+    _releaseRequestContext();
+
+    // access skin from external provider
+    _initRequestContextAndGenerateCss("purple");
+    _verifyNumberOfSkinInCache(2);
+    _verifyNumberOfCssFiles(6);
+    _releaseRequestContext();
+
+    // access skin from external provider
+    _initRequestContextAndGenerateCss("violet");
+    _verifyNumberOfSkinInCache(2);
+    _verifyNumberOfCssFiles(7);
+    _releaseRequestContext();
+  }
+
+  private void _verifyNumberOfCssFiles(int skinNumber)
+  {
+    assertEquals("Skin file not generated as expected.", skinNumber, _getTempDir().list().length);
+  }
+
+  private void _verifyNumberOfSkinInCache(int skinNumber)
+  {
+    assertEquals("StyleProvider cache size does not match.", skinNumber, _getStyleProviderCacheSize());
+  }
+
+  private int _getStyleProviderCacheSize()
+  {
+    ConcurrentMap<String, Object> appMap = RequestContext.getCurrentInstance().getApplicationScopedConcurrentMap();
+    Object cache = appMap.get(_SKIN_PROVIDERS_CACHE_KEY);
+    if (cache == null)
+      fail("Skin provider cache cannot be null.");
+    if (!(cache instanceof Map))
+      fail("Skin provider cache should be a map.");
+    return ((Map) appMap.get(_SKIN_PROVIDERS_CACHE_KEY)).keySet().size();
+  }
+
+  private File _getTempDir()
+  {
+    Map<String, Object> applicationMap = _externalContext.getApplicationMap();
+    String path = ((File) applicationMap.get("javax.servlet.context.tempdir")).getAbsolutePath() + "/adf/styles/cache/";
+    File tempDir = new File(path);
+
+    if (!tempDir.exists())
+      tempDir.mkdirs();
+
+    return tempDir;
+  }
+
+  private void _cleanTempDir()
+  {
+    File tempDir = _getTempDir();
+    System.out.println("Cleaning temp directory: " + tempDir.getAbsolutePath());
+
+    if (tempDir.exists())
+      for (File file : tempDir.listFiles())
+        file.delete();
+  }
+
+  private Skin _testUsingId(String skinId)
+  {
+    SkinMetadata skinMetadata = new SkinMetadata.Builder().id(skinId).build();
+    Skin skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertEquals("Unexpected skin id.", skinId, skin.getId());
+    return skin;
+  }
+
+  private Skin _testUsingFamily(String family,
+                                String expectedSkinId)
+  {
+    SkinMetadata skinMetadata = new SkinMetadata.Builder().family(family).build();
+    Skin skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertEquals("Unexpected skin id.", expectedSkinId, skin.getId());
+    assertEquals("Unexpected skin renderKitId.", SkinMetadata.RenderKitId.DESKTOP.toString(), skin.getRenderKitId());
+    return skin;
+  }
+
+  private Skin _testUsingFamilyAndRenderkit(String family,
+                                            String renderkitId,
+                                            String expectedSkinId)
+  {
+    SkinMetadata skinMetadata = new SkinMetadata.Builder()
+        .family(family)
+        .renderKitId(SkinMetadata.RenderKitId.fromId(renderkitId))
+        .build();
+    Skin skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertEquals("Unexpected skin id.", expectedSkinId, skin.getId());
+    assertEquals("Unexpected skin renderKitId.", renderkitId, skin.getRenderKitId());
+    return skin;
+  }
+
+  private Skin _testUsingFamilyAndVersion(String family,
+                                          SkinVersion version,
+                                          String expectedSkinId)
+  {
+    SkinMetadata skinMetadata = new SkinMetadata.Builder()
+        .family(family)
+        .version(version)
+        .build();
+    Skin skin = _provider.getSkin(_externalContext, skinMetadata);
+    assertEquals("Unexpected skin id.", expectedSkinId, skin.getId());
+    assertEquals("Unexpected skin renderKitId.", SkinMetadata.RenderKitId.DESKTOP.toString(), skin.getRenderKitId());
+    return skin;
+  }
+
+  /**
+   * This method sets skinFamily into request context and executes
+   * encode of a simple page. This results in generation of the css
+   * for the skin family specified.
+   * This code is similar to what we do in RenderKitTestCase.BaseTest
+   * @see org.apache.myfaces.trinidadinternal.renderkit.RenderKitTestCase.BaseTest
+   * @param skinFamily
+   */
+  private void _initRequestContextAndGenerateCss(String skinFamily)
+  {
+    try
+    {
+      _requestContext = new MRequestContext();
+      _requestContext.setSkinFamily(skinFamily);
+      _requestContext.setRightToLeft(false);
+      _requestContext.setAgent(RenderKitBootstrap.getGeckoAgent());
+
+      UIViewRoot root = RenderKitBootstrap.createUIViewRoot(_facesContext);
+      root.setRenderKitId("org.apache.myfaces.trinidad.core");
+      _facesContext.setViewRoot(root);
+      _facesContext.setResponseWriter(new TestResponseWriter(new NullWriter(),
+                                                             XhtmlResponseWriter.XHTML_CONTENT_TYPE,
+                                                             "UTF-8",
+                                                             this,
+                                                             null));
+      CoreDocument doc = new CoreDocument();
+      doc.setId("docId");
+      root.getChildren().add(doc);
+      CoreForm form = new CoreForm();
+      form.setId("formId");
+      doc.getChildren().add(form);
+
+      ExtendedRenderKitService service = Service.getService(_facesContext.getRenderKit(),
+                                                            ExtendedRenderKitService.class);
+      if (service != null)
+        service.encodeBegin(_facesContext);
+
+      RenderUtils.encodeRecursive(_facesContext, root);
+    }
+    catch (Exception e)
+    {
+      System.out.println("Exception in _initRequestContextAndGenerateCss");
+      e.printStackTrace();
+    }
+  }
+
+  private void _releaseRequestContext()
+  {
+    _requestContext.release();
+    _requestContext = null;
+  }
+
+  private        MRequestContext      _requestContext;
+  private        MFacesContext        _facesContext;
+  private        SkinProvider         _provider;
+  private        ExternalContext      _externalContext;
+
+  private static FacesConfigInfo      _facesConfigInfo;
+
+  private static final String _SKIN_PROVIDERS_CACHE_KEY = "org.apache.myfaces.trinidadinternal.skin.SKIN_PROVIDERS_KEY";
+
+  static
+  {
+    try
+    {
+      RenderKitBootstrap bootstrap = new RenderKitBootstrap();
+      bootstrap.init();
+      _facesConfigInfo = bootstrap.getFacesConfigInfo();
+    }
+    catch (Exception e)
+    {
+    }
+  }
+}

Added: myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMapTest.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMapTest.java?rev=1632265&view=auto
==============================================================================
--- myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMapTest.java (added)
+++ myfaces/trinidad/trunk/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/CopyOnWriteArrayMapTest.java Thu Oct 16 10:16:54 2014
@@ -0,0 +1,28 @@
+package org.apache.myfaces.trinidadinternal.util;
+
+import org.apache.myfaces.trinidadbuild.test.ConcurrentMapTestCase;
+
+import java.util.concurrent.ConcurrentMap;
+
+public class CopyOnWriteArrayMapTest extends ConcurrentMapTestCase
+{
+  public CopyOnWriteArrayMapTest(String testName)
+  {
+    super(testName);
+  }
+
+  protected boolean supportsNullKeys()
+  {
+    return false;
+  }
+
+  protected ConcurrentMap<String, Object> createMap()
+  {
+    return CopyOnWriteArrayMap.newConcurrentMap();
+  }
+
+  protected ConcurrentMap<LameKey, Object> createMapWithLameKey()
+  {
+    return CopyOnWriteArrayMap.newConcurrentMap();
+  }
+}