View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.util;
18  
19  import java.io.IOException;
20  import java.io.InvalidObjectException;
21  import java.util.Arrays;
22  import java.util.ConcurrentModificationException;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.Objects;
26  
27  /**
28   * <em>Consider this class private.</em>
29   * Array-based implementation of the {@code ReadOnlyStringMap} interface. Keys are held in a sorted array.
30   * <p>
31   * This is not a generic collection, but makes some trade-offs to optimize for the Log4j context data use case:
32   * </p>
33   * <ul>
34   *   <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li>
35   *   <li>Fast copy. If the ThreadContextMap is also an instance of {@code SortedArrayStringMap}, the full thread context
36   *     data can be transferred with two array copies and two field updates.</li>
37   *   <li>Acceptable performance for small data sets. The current implementation stores keys in a sorted array, values
38   *     are stored in a separate array at the same index.
39   *     Worst-case performance of {@code get} and {@code containsKey} is O(log N),
40   *     worst-case performance of {@code put} and {@code remove} is O(N log N).
41   *     The expectation is that for the small values of {@code N} (less than 100) that are the vast majority of
42   *     ThreadContext use cases, the constants dominate performance more than the asymptotic performance of the
43   *     algorithms used.
44   *     </li>
45   *     <li>Compact representation.</li>
46   * </ul>
47   *
48   * @since 2.7
49   */
50  public class SortedArrayStringMap implements StringMap {
51  
52      /**
53       * The default initial capacity.
54       */
55      private static final int DEFAULT_INITIAL_CAPACITY = 4;
56      private static final long serialVersionUID = -5748905872274478116L;
57      private static final int HASHVAL = 31;
58  
59      private static final TriConsumer<String, Object, StringMap> PUT_ALL = new TriConsumer<String, Object, StringMap>() {
60          @Override
61          public void accept(final String key, final Object value, final StringMap contextData) {
62              contextData.putValue(key, value);
63          }
64      };
65  
66      /**
67       * An empty array instance to share when the table is not inflated.
68       */
69      private static final String[] EMPTY = {};
70      private static final String FROZEN = "Frozen collection cannot be modified";
71  
72      private transient String[] keys = EMPTY;
73      private transient Object[] values = EMPTY;
74  
75      /**
76       * The number of key-value mappings contained in this map.
77       */
78      private transient int size;
79  
80      /**
81       * The next size value at which to resize (capacity * load factor).
82       * @serial
83       */
84      // If table == EMPTY_TABLE then this is the initial capacity at which the
85      // table will be created when inflated.
86      private int threshold;
87      private boolean immutable;
88      private transient boolean iterating;
89  
90      public SortedArrayStringMap() {
91          this(DEFAULT_INITIAL_CAPACITY);
92      }
93  
94      public SortedArrayStringMap(final int initialCapacity) {
95          if (initialCapacity < 1) {
96              throw new IllegalArgumentException("Initial capacity must be at least one but was " + initialCapacity);
97          }
98          threshold = ceilingNextPowerOfTwo(initialCapacity);
99      }
100 
101     public SortedArrayStringMap(final ReadOnlyStringMap other) {
102         if (other instanceof SortedArrayStringMap) {
103             initFrom0((SortedArrayStringMap) other);
104         } else if (other != null) {
105             resize(ceilingNextPowerOfTwo(other.size()));
106             other.forEach(PUT_ALL, this);
107         }
108     }
109 
110     private void assertNotFrozen() {
111         if (immutable) {
112             throw new UnsupportedOperationException(FROZEN);
113         }
114     }
115 
116     private void assertNoConcurrentModification() {
117         if (iterating) {
118             throw new ConcurrentModificationException();
119         }
120     }
121 
122     @Override
123     public void clear() {
124         if (keys == EMPTY) {
125             return;
126         }
127         assertNotFrozen();
128         assertNoConcurrentModification();
129 
130         Arrays.fill(keys, 0, size, null);
131         Arrays.fill(values, 0, size, null);
132         size = 0;
133     }
134 
135     @Override
136     public boolean containsKey(final String key) {
137         return indexOfKey(key) >= 0;
138     }
139 
140     @Override
141     public Map<String, String> toMap() {
142         final Map<String, String> result = new HashMap<>(size());
143         for (int i = 0; i < size(); i++) {
144             final Object value = getValueAt(i);
145             result.put(getKeyAt(i), value == null ? null : String.valueOf(value));
146         }
147         return result;
148     }
149 
150     @Override
151     public void freeze() {
152         immutable = true;
153     }
154 
155     @Override
156     public boolean isFrozen() {
157         return immutable;
158     }
159 
160     @SuppressWarnings("unchecked")
161     @Override
162     public <V> V getValue(final String key) {
163         final int index = indexOfKey(key);
164         if (index < 0) {
165             return null;
166         }
167         return (V) values[index];
168     }
169 
170     @Override
171     public boolean isEmpty() {
172         return size == 0;
173     }
174 
175     int indexOfKey(final String key) {
176         if (keys == EMPTY) {
177             return -1;
178         }
179         if (key == null) { // null key is located at the start of the array
180             return nullKeyIndex(); // insert at index zero
181         }
182         final int start = size > 0 && keys[0] == null ? 1 : 0;
183         return Arrays.binarySearch(keys, start, size, key);
184     }
185 
186     private int nullKeyIndex() {
187         return size > 0 && keys[0] == null ? 0 : ~0;
188     }
189 
190     @Override
191     public void putValue(final String key, final Object value) {
192         assertNotFrozen();
193         assertNoConcurrentModification();
194 
195         if (keys == EMPTY) {
196             inflateTable(threshold);
197         }
198         final int index = indexOfKey(key);
199         if (index >= 0) {
200             keys[index] = key;
201             values[index] = value;
202         } else { // not found, so insert.
203             insertAt(~index, key, value);
204         }
205     }
206 
207     private void insertAt(final int index, final String key, final Object value) {
208         ensureCapacity();
209         System.arraycopy(keys, index, keys, index + 1, size - index);
210         System.arraycopy(values, index, values, index + 1, size - index);
211         keys[index] = key;
212         values[index] = value;
213         size++;
214     }
215 
216     @Override
217     public void putAll(final ReadOnlyStringMap source) {
218         if (source == this || source.isEmpty()) { // throw NPE if null
219             return; // this.putAll(this) does not modify this collection
220         }
221         assertNotFrozen();
222         assertNoConcurrentModification();
223 
224         if (source instanceof SortedArrayStringMap) {
225             if (this.size == 0) {
226                 initFrom0((SortedArrayStringMap) source);
227             } else {
228                 merge((SortedArrayStringMap) source);
229             }
230         } else if (source != null) {
231             source.forEach(PUT_ALL, this);
232         }
233     }
234 
235     private void initFrom0(final SortedArrayStringMap other) {
236         if (keys.length < other.size) {
237             keys = new String[other.threshold];
238             values = new Object[other.threshold];
239         }
240         System.arraycopy(other.keys, 0, keys, 0, other.size);
241         System.arraycopy(other.values, 0, values, 0, other.size);
242 
243         size = other.size;
244         threshold = other.threshold;
245     }
246 
247     private void merge(final SortedArrayStringMap other) {
248         final String[] myKeys = keys;
249         final Object[] myVals = values;
250         final int newSize = other.size + this.size;
251         threshold = ceilingNextPowerOfTwo(newSize);
252         if (keys.length < threshold) {
253             keys = new String[threshold];
254             values = new Object[threshold];
255         }
256         // move largest collection to the beginning of this data structure, smallest to the end
257         boolean overwrite = true;
258         if (other.size() > size()) {
259             // move original key-values to end
260             System.arraycopy(myKeys, 0, keys, other.size, this.size);
261             System.arraycopy(myVals, 0, values, other.size, this.size);
262 
263             // insert additional key-value pairs at the beginning
264             System.arraycopy(other.keys, 0, keys, 0, other.size);
265             System.arraycopy(other.values, 0, values, 0, other.size);
266             size = other.size;
267 
268             // loop over original keys and insert (drop values for same key)
269             overwrite = false;
270         } else {
271             System.arraycopy(myKeys, 0, keys, 0, this.size);
272             System.arraycopy(myVals, 0, values, 0, this.size);
273 
274             // move additional key-value pairs to end
275             System.arraycopy(other.keys, 0, keys, this.size, other.size);
276             System.arraycopy(other.values, 0, values, this.size, other.size);
277 
278             // new values are at the end, will be processed below. Overwrite is true.
279         }
280         for (int i = size; i < newSize; i++) {
281             final int index = indexOfKey(keys[i]);
282             if (index < 0) { // key does not exist
283                 insertAt(~index, keys[i], values[i]);
284             } else if (overwrite) { // existing key: only overwrite when looping over the new values
285                 keys[index] = keys[i];
286                 values[index] = values[i];
287             }
288         }
289         // prevent memory leak: null out references
290         Arrays.fill(keys, size, newSize, null);
291         Arrays.fill(values, size, newSize, null);
292     }
293 
294     private void ensureCapacity() {
295         if (size >= threshold) {
296             resize(threshold * 2);
297         }
298     }
299 
300     private void resize(final int newCapacity) {
301         final String[] oldKeys = keys;
302         final Object[] oldValues = values;
303 
304         keys = new String[newCapacity];
305         values = new Object[newCapacity];
306 
307         System.arraycopy(oldKeys, 0, keys, 0, size);
308         System.arraycopy(oldValues, 0, values, 0, size);
309 
310         threshold = newCapacity;
311     }
312 
313     /**
314      * Inflates the table.
315      */
316     private void inflateTable(final int toSize) {
317         threshold = toSize;
318         keys = new String[toSize];
319         values = new Object[toSize];
320     }
321 
322     @Override
323     public void remove(final String key) {
324         if (keys == EMPTY) {
325             return;
326         }
327 
328         final int index = indexOfKey(key);
329         if (index >= 0) {
330             assertNotFrozen();
331             assertNoConcurrentModification();
332 
333             System.arraycopy(keys, index + 1, keys, index, size - 1 - index);
334             System.arraycopy(values, index + 1, values, index, size - 1 - index);
335             keys[size - 1] = null;
336             values[size - 1] = null;
337             size--;
338         }
339     }
340 
341     String getKeyAt(final int index) {
342         if (index < 0 || index >= size) {
343             return null;
344         }
345         return keys[index];
346     }
347 
348     @SuppressWarnings("unchecked")
349     <V> V getValueAt(final int index) {
350         if (index < 0 || index >= size) {
351             return null;
352         }
353         return (V) values[index];
354     }
355 
356     @Override
357     public int size() {
358         return size;
359     }
360 
361     @SuppressWarnings("unchecked")
362     @Override
363     public <V> void forEach(final BiConsumer<String, ? super V> action) {
364         iterating = true;
365         try {
366             for (int i = 0; i < size; i++) {
367                 action.accept(keys[i], (V) values[i]);
368             }
369         } finally {
370             iterating = false;
371         }
372     }
373 
374     @SuppressWarnings("unchecked")
375     @Override
376     public <V, T> void forEach(final TriConsumer<String, ? super V, T> action, final T state) {
377         iterating = true;
378         try {
379             for (int i = 0; i < size; i++) {
380                 action.accept(keys[i], (V) values[i], state);
381             }
382         } finally {
383             iterating = false;
384         }
385     }
386 
387     @Override
388     public boolean equals(final Object obj) {
389         if (obj == this) {
390             return true;
391         }
392         if (!(obj instanceof SortedArrayStringMap)) {
393             return false;
394         }
395         final SortedArrayStringMap other = (SortedArrayStringMap) obj;
396         if (this.size() != other.size()) {
397             return false;
398         }
399         for (int i = 0; i < size(); i++) {
400             if (!Objects.equals(keys[i], other.keys[i])) {
401                 return false;
402             }
403             if (!Objects.equals(values[i], other.values[i])) {
404                 return false;
405             }
406         }
407         return true;
408     }
409 
410     @Override
411     public int hashCode() {
412         int result = 37;
413         result = HASHVAL * result + size;
414         result = HASHVAL * result + hashCode(keys, size);
415         result = HASHVAL * result + hashCode(values, size);
416         return result;
417     }
418 
419     private static int hashCode(final Object[] values, final int length) {
420         int result = 1;
421         for (int i = 0; i < length; i++) {
422             result = HASHVAL * result + (values[i] == null ? 0 : values[i].hashCode());
423         }
424         return result;
425     }
426 
427     @Override
428     public String toString() {
429         final StringBuilder sb = new StringBuilder(256);
430         sb.append('{');
431         for (int i = 0; i < size; i++) {
432             if (i > 0) {
433                 sb.append(", ");
434             }
435             sb.append(keys[i]).append('=');
436             sb.append(values[i] == this ? "(this map)" : values[i]);
437         }
438         sb.append('}');
439         return sb.toString();
440     }
441 
442     /**
443      * Save the state of the {@code SortedArrayStringMap} instance to a stream (i.e.,
444      * serialize it).
445      *
446      * @serialData The <i>capacity</i> of the SortedArrayStringMap (the length of the
447      *             bucket array) is emitted (int), followed by the
448      *             <i>size</i> (an int, the number of key-value
449      *             mappings), followed by the key (Object) and value (Object)
450      *             for each key-value mapping.  The key-value mappings are
451      *             emitted in no particular order.
452      */
453     private void writeObject(final java.io.ObjectOutputStream s) throws IOException {
454         // Write out the threshold, and any hidden stuff
455         s.defaultWriteObject();
456 
457         // Write out number of buckets
458         if (keys == EMPTY) {
459             s.writeInt(ceilingNextPowerOfTwo(threshold));
460         } else {
461             s.writeInt(keys.length);
462         }
463 
464         // Write out size (number of Mappings)
465         s.writeInt(size);
466 
467         // Write out keys and values (alternating)
468         if (size > 0) {
469             for (int i = 0; i < size; i++) {
470                 s.writeObject(keys[i]);
471                 s.writeObject(values[i]);
472             }
473         }
474     }
475 
476 
477     /**
478      * Calculate the next power of 2, greater than or equal to x.
479      * <p>
480      * From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
481      *
482      * @param x Value to round up
483      * @return The next power of 2 from x inclusive
484      */
485     private static int ceilingNextPowerOfTwo(final int x) {
486         final int BITS_PER_INT = 32;
487         return 1 << (BITS_PER_INT - Integer.numberOfLeadingZeros(x - 1));
488     }
489 
490     /**
491      * Reconstitute the {@code SortedArrayStringMap} instance from a stream (i.e.,
492      * deserialize it).
493      */
494     private void readObject(final java.io.ObjectInputStream s)  throws IOException, ClassNotFoundException {
495         // Read in the threshold (ignored), and any hidden stuff
496         s.defaultReadObject();
497 
498         // set other fields that need values
499         keys = EMPTY;
500         values = EMPTY;
501 
502         // Read in number of buckets
503         final int capacity = s.readInt();
504         if (capacity < 0) {
505             throw new InvalidObjectException("Illegal capacity: " + capacity);
506         }
507 
508         // Read number of mappings
509         final int mappings = s.readInt();
510         if (mappings < 0) {
511             throw new InvalidObjectException("Illegal mappings count: " + mappings);
512         }
513 
514         // allocate the bucket array;
515         if (mappings > 0) {
516             inflateTable(capacity);
517         } else {
518             threshold = capacity;
519         }
520 
521         // Read the keys and values, and put the mappings in the arrays
522         for (int i = 0; i < mappings; i++) {
523             keys[i] = (String) s.readObject();
524             values[i] = s.readObject();
525         }
526         size = mappings;
527     }
528 }