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.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.InvalidObjectException;
23  import java.io.ObjectInputStream;
24  import java.io.ObjectOutputStream;
25  import java.io.StreamCorruptedException;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.lang.reflect.Modifier;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.ConcurrentModificationException;
32  import java.util.HashMap;
33  import java.util.Map;
34  import java.util.Objects;
35  
36  import org.apache.logging.log4j.status.StatusLogger;
37  
38  /**
39   * <em>Consider this class private.</em>
40   * Array-based implementation of the {@code ReadOnlyStringMap} interface. Keys are held in a sorted array.
41   * <p>
42   * This is not a generic collection, but makes some trade-offs to optimize for the Log4j context data use case:
43   * </p>
44   * <ul>
45   *   <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li>
46   *   <li>Fast copy. If the ThreadContextMap is also an instance of {@code SortedArrayStringMap}, the full thread context
47   *     data can be transferred with two array copies and two field updates.</li>
48   *   <li>Acceptable performance for small data sets. The current implementation stores keys in a sorted array, values
49   *     are stored in a separate array at the same index.
50   *     Worst-case performance of {@code get} and {@code containsKey} is O(log N),
51   *     worst-case performance of {@code put} and {@code remove} is O(N log N).
52   *     The expectation is that for the small values of {@code N} (less than 100) that are the vast majority of
53   *     ThreadContext use cases, the constants dominate performance more than the asymptotic performance of the
54   *     algorithms used.
55   *     </li>
56   *     <li>Compact representation.</li>
57   * </ul>
58   *
59   * @since 2.7
60   */
61  public class SortedArrayStringMap implements IndexedStringMap {
62  
63      /**
64       * The default initial capacity.
65       */
66      private static final int DEFAULT_INITIAL_CAPACITY = 4;
67      private static final long serialVersionUID = -5748905872274478116L;
68      private static final int HASHVAL = 31;
69  
70      private static final TriConsumer<String, Object, StringMap> PUT_ALL = (key, value, contextData) -> contextData.putValue(key, value);
71  
72      /**
73       * An empty array instance to share when the table is not inflated.
74       */
75      private static final String[] EMPTY = {};
76      private static final String FROZEN = "Frozen collection cannot be modified";
77  
78      private transient String[] keys = EMPTY;
79      private transient Object[] values = EMPTY;
80  
81      /**
82       * The number of key-value mappings contained in this map.
83       */
84      private transient int size;
85  
86      private static final Method setObjectInputFilter;
87      private static final Method getObjectInputFilter;
88      private static final Method newObjectInputFilter;
89  
90      static {
91          Method[] methods = ObjectInputStream.class.getMethods();
92          Method setMethod = null;
93          Method getMethod = null;
94          for (final Method method : methods) {
95              if (method.getName().equals("setObjectInputFilter")) {
96                  setMethod = method;
97              } else if (method.getName().equals("getObjectInputFilter")) {
98                  getMethod = method;
99              }
100         }
101         Method newMethod = null;
102         try {
103             if (setMethod != null) {
104                 final Class<?> clazz = Class.forName("org.apache.logging.log4j.util.internal.DefaultObjectInputFilter");
105                 methods = clazz.getMethods();
106                 for (final Method method : methods) {
107                     if (method.getName().equals("newInstance") && Modifier.isStatic(method.getModifiers())) {
108                         newMethod = method;
109                         break;
110                     }
111                 }
112             }
113         } catch (final ClassNotFoundException ex) {
114             // Ignore the exception
115         }
116         newObjectInputFilter = newMethod;
117         setObjectInputFilter = setMethod;
118         getObjectInputFilter = getMethod;
119     }
120 
121     /**
122      * The next size value at which to resize (capacity * load factor).
123      * @serial
124      */
125     // If table == EMPTY_TABLE then this is the initial capacity at which the
126     // table will be created when inflated.
127     private int threshold;
128     private boolean immutable;
129     private transient boolean iterating;
130 
131     public SortedArrayStringMap() {
132         this(DEFAULT_INITIAL_CAPACITY);
133     }
134 
135     public SortedArrayStringMap(final int initialCapacity) {
136         if (initialCapacity < 0) {
137             throw new IllegalArgumentException("Initial capacity must be at least zero but was " + initialCapacity);
138         }
139         threshold = ceilingNextPowerOfTwo(initialCapacity == 0 ? 1 : initialCapacity);
140     }
141 
142     public SortedArrayStringMap(final ReadOnlyStringMap other) {
143         if (other instanceof SortedArrayStringMap) {
144             initFrom0((SortedArrayStringMap) other);
145         } else if (other != null) {
146             resize(ceilingNextPowerOfTwo(other.size()));
147             other.forEach(PUT_ALL, this);
148         }
149     }
150 
151     public SortedArrayStringMap(final Map<String, ?> map) {
152         resize(ceilingNextPowerOfTwo(map.size()));
153         for (final Map.Entry<String, ?> entry : map.entrySet()) {
154             putValue(entry.getKey(), entry.getValue());
155         }
156     }
157 
158     private void assertNotFrozen() {
159         if (immutable) {
160             throw new UnsupportedOperationException(FROZEN);
161         }
162     }
163 
164     private void assertNoConcurrentModification() {
165         if (iterating) {
166             throw new ConcurrentModificationException();
167         }
168     }
169 
170     @Override
171     public void clear() {
172         if (keys == EMPTY) {
173             return;
174         }
175         assertNotFrozen();
176         assertNoConcurrentModification();
177 
178         Arrays.fill(keys, 0, size, null);
179         Arrays.fill(values, 0, size, null);
180         size = 0;
181     }
182 
183     @Override
184     public boolean containsKey(final String key) {
185         return indexOfKey(key) >= 0;
186     }
187 
188     @Override
189     public Map<String, String> toMap() {
190         final Map<String, String> result = new HashMap<>(size());
191         for (int i = 0; i < size(); i++) {
192             final Object value = getValueAt(i);
193             result.put(getKeyAt(i), value == null ? null : String.valueOf(value));
194         }
195         return result;
196     }
197 
198     @Override
199     public void freeze() {
200         immutable = true;
201     }
202 
203     @Override
204     public boolean isFrozen() {
205         return immutable;
206     }
207 
208     @SuppressWarnings("unchecked")
209     @Override
210     public <V> V getValue(final String key) {
211         final int index = indexOfKey(key);
212         if (index < 0) {
213             return null;
214         }
215         return (V) values[index];
216     }
217 
218     @Override
219     public boolean isEmpty() {
220         return size == 0;
221     }
222 
223     @Override
224     public int indexOfKey(final String key) {
225         if (keys == EMPTY) {
226             return -1;
227         }
228         if (key == null) { // null key is located at the start of the array
229             return nullKeyIndex(); // insert at index zero
230         }
231         final int start = size > 0 && keys[0] == null ? 1 : 0;
232         return Arrays.binarySearch(keys, start, size, key);
233     }
234 
235     private int nullKeyIndex() {
236         return size > 0 && keys[0] == null ? 0 : ~0;
237     }
238 
239     @Override
240     public void putValue(final String key, final Object value) {
241         assertNotFrozen();
242         assertNoConcurrentModification();
243 
244         if (keys == EMPTY) {
245             inflateTable(threshold);
246         }
247         final int index = indexOfKey(key);
248         if (index >= 0) {
249             keys[index] = key;
250             values[index] = value;
251         } else { // not found, so insert.
252             insertAt(~index, key, value);
253         }
254     }
255 
256     private void insertAt(final int index, final String key, final Object value) {
257         ensureCapacity();
258         System.arraycopy(keys, index, keys, index + 1, size - index);
259         System.arraycopy(values, index, values, index + 1, size - index);
260         keys[index] = key;
261         values[index] = value;
262         size++;
263     }
264 
265     @Override
266     public void putAll(final ReadOnlyStringMap source) {
267         if (source == this || source == null || source.isEmpty()) {
268             // this.putAll(this) does not modify this collection
269             // this.putAll(null) does not modify this collection
270             // this.putAll(empty ReadOnlyStringMap) does not modify this collection
271             return;
272         }
273         assertNotFrozen();
274         assertNoConcurrentModification();
275 
276         if (source instanceof SortedArrayStringMap) {
277             if (this.size == 0) {
278                 initFrom0((SortedArrayStringMap) source);
279             } else {
280                 merge((SortedArrayStringMap) source);
281             }
282         } else if (source != null) {
283             source.forEach(PUT_ALL, this);
284         }
285     }
286 
287     private void initFrom0(final SortedArrayStringMap other) {
288         if (keys.length < other.size) {
289             keys = new String[other.threshold];
290             values = new Object[other.threshold];
291         }
292         System.arraycopy(other.keys, 0, keys, 0, other.size);
293         System.arraycopy(other.values, 0, values, 0, other.size);
294 
295         size = other.size;
296         threshold = other.threshold;
297     }
298 
299     private void merge(final SortedArrayStringMap other) {
300         final String[] myKeys = keys;
301         final Object[] myVals = values;
302         final int newSize = other.size + this.size;
303         threshold = ceilingNextPowerOfTwo(newSize);
304         if (keys.length < threshold) {
305             keys = new String[threshold];
306             values = new Object[threshold];
307         }
308         // move largest collection to the beginning of this data structure, smallest to the end
309         boolean overwrite = true;
310         if (other.size() > size()) {
311             // move original key-values to end
312             System.arraycopy(myKeys, 0, keys, other.size, this.size);
313             System.arraycopy(myVals, 0, values, other.size, this.size);
314 
315             // insert additional key-value pairs at the beginning
316             System.arraycopy(other.keys, 0, keys, 0, other.size);
317             System.arraycopy(other.values, 0, values, 0, other.size);
318             size = other.size;
319 
320             // loop over original keys and insert (drop values for same key)
321             overwrite = false;
322         } else {
323             System.arraycopy(myKeys, 0, keys, 0, this.size);
324             System.arraycopy(myVals, 0, values, 0, this.size);
325 
326             // move additional key-value pairs to end
327             System.arraycopy(other.keys, 0, keys, this.size, other.size);
328             System.arraycopy(other.values, 0, values, this.size, other.size);
329 
330             // new values are at the end, will be processed below. Overwrite is true.
331         }
332         for (int i = size; i < newSize; i++) {
333             final int index = indexOfKey(keys[i]);
334             if (index < 0) { // key does not exist
335                 insertAt(~index, keys[i], values[i]);
336             } else if (overwrite) { // existing key: only overwrite when looping over the new values
337                 keys[index] = keys[i];
338                 values[index] = values[i];
339             }
340         }
341         // prevent memory leak: null out references
342         Arrays.fill(keys, size, newSize, null);
343         Arrays.fill(values, size, newSize, null);
344     }
345 
346     private void ensureCapacity() {
347         if (size >= threshold) {
348             resize(threshold * 2);
349         }
350     }
351 
352     private void resize(final int newCapacity) {
353         final String[] oldKeys = keys;
354         final Object[] oldValues = values;
355 
356         keys = new String[newCapacity];
357         values = new Object[newCapacity];
358 
359         System.arraycopy(oldKeys, 0, keys, 0, size);
360         System.arraycopy(oldValues, 0, values, 0, size);
361 
362         threshold = newCapacity;
363     }
364 
365     /**
366      * Inflates the table.
367      */
368     private void inflateTable(final int toSize) {
369         threshold = toSize;
370         keys = new String[toSize];
371         values = new Object[toSize];
372     }
373 
374     @Override
375     public void remove(final String key) {
376         if (keys == EMPTY) {
377             return;
378         }
379 
380         final int index = indexOfKey(key);
381         if (index >= 0) {
382             assertNotFrozen();
383             assertNoConcurrentModification();
384 
385             System.arraycopy(keys, index + 1, keys, index, size - 1 - index);
386             System.arraycopy(values, index + 1, values, index, size - 1 - index);
387             keys[size - 1] = null;
388             values[size - 1] = null;
389             size--;
390         }
391     }
392 
393     @Override
394     public String getKeyAt(final int index) {
395         if (index < 0 || index >= size) {
396             return null;
397         }
398         return keys[index];
399     }
400 
401     @SuppressWarnings("unchecked")
402     @Override
403     public <V> V getValueAt(final int index) {
404         if (index < 0 || index >= size) {
405             return null;
406         }
407         return (V) values[index];
408     }
409 
410     @Override
411     public int size() {
412         return size;
413     }
414 
415     @SuppressWarnings("unchecked")
416     @Override
417     public <V> void forEach(final BiConsumer<String, ? super V> action) {
418         iterating = true;
419         try {
420             for (int i = 0; i < size; i++) {
421                 action.accept(keys[i], (V) values[i]);
422             }
423         } finally {
424             iterating = false;
425         }
426     }
427 
428     @SuppressWarnings("unchecked")
429     @Override
430     public <V, T> void forEach(final TriConsumer<String, ? super V, T> action, final T state) {
431         iterating = true;
432         try {
433             for (int i = 0; i < size; i++) {
434                 action.accept(keys[i], (V) values[i], state);
435             }
436         } finally {
437             iterating = false;
438         }
439     }
440 
441     @Override
442     public boolean equals(final Object obj) {
443         if (obj == this) {
444             return true;
445         }
446         if (!(obj instanceof SortedArrayStringMap)) {
447             return false;
448         }
449         final SortedArrayStringMap other = (SortedArrayStringMap) obj;
450         if (this.size() != other.size()) {
451             return false;
452         }
453         for (int i = 0; i < size(); i++) {
454             if (!Objects.equals(keys[i], other.keys[i])) {
455                 return false;
456             }
457             if (!Objects.equals(values[i], other.values[i])) {
458                 return false;
459             }
460         }
461         return true;
462     }
463 
464     @Override
465     public int hashCode() {
466         int result = 37;
467         result = HASHVAL * result + size;
468         result = HASHVAL * result + hashCode(keys, size);
469         result = HASHVAL * result + hashCode(values, size);
470         return result;
471     }
472 
473     private static int hashCode(final Object[] values, final int length) {
474         int result = 1;
475         for (int i = 0; i < length; i++) {
476             result = HASHVAL * result + (values[i] == null ? 0 : values[i].hashCode());
477         }
478         return result;
479     }
480 
481     @Override
482     public String toString() {
483         final StringBuilder sb = new StringBuilder(256);
484         sb.append('{');
485         for (int i = 0; i < size; i++) {
486             if (i > 0) {
487                 sb.append(", ");
488             }
489             sb.append(keys[i]).append('=');
490             sb.append(values[i] == this ? "(this map)" : values[i]);
491         }
492         sb.append('}');
493         return sb.toString();
494     }
495 
496     /**
497      * Save the state of the {@code SortedArrayStringMap} instance to a stream (i.e.,
498      * serialize it).
499      *
500      * @serialData The <i>capacity</i> of the SortedArrayStringMap (the length of the
501      *             bucket array) is emitted (int), followed by the
502      *             <i>size</i> (an int, the number of key-value
503      *             mappings), followed by the key (Object) and value (Object)
504      *             for each key-value mapping.  The key-value mappings are
505      *             emitted in no particular order.
506      */
507     private void writeObject(final java.io.ObjectOutputStream s) throws IOException {
508         // Write out the threshold, and any hidden stuff
509         s.defaultWriteObject();
510 
511         // Write out number of buckets
512         if (keys == EMPTY) {
513             s.writeInt(ceilingNextPowerOfTwo(threshold));
514         } else {
515             s.writeInt(keys.length);
516         }
517 
518         // Write out size (number of Mappings)
519         s.writeInt(size);
520 
521         // Write out keys and values (alternating)
522         if (size > 0) {
523             for (int i = 0; i < size; i++) {
524                 s.writeObject(keys[i]);
525                 try {
526                     s.writeObject(marshall(values[i]));
527                 } catch (final Exception e) {
528                     handleSerializationException(e, i, keys[i]);
529                     s.writeObject(null);
530                 }
531             }
532         }
533     }
534 
535     private static byte[] marshall(final Object obj) throws IOException {
536         if (obj == null) {
537             return null;
538         }
539         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
540         try (ObjectOutputStream oos = new ObjectOutputStream(bout)) {
541             oos.writeObject(obj);
542             oos.flush();
543             return bout.toByteArray();
544         }
545     }
546 
547     private static Object unmarshall(final byte[] data, final ObjectInputStream inputStream)
548             throws IOException, ClassNotFoundException {
549         final ByteArrayInputStream bin = new ByteArrayInputStream(data);
550         Collection<String> allowedClasses = null;
551         ObjectInputStream ois;
552         if (inputStream instanceof FilteredObjectInputStream) {
553             allowedClasses = ((FilteredObjectInputStream) inputStream).getAllowedClasses();
554             ois = new FilteredObjectInputStream(bin, allowedClasses);
555         } else {
556             try {
557                 final Object obj = getObjectInputFilter.invoke(inputStream);
558                 final Object filter = newObjectInputFilter.invoke(null, obj);
559                 ois = new ObjectInputStream(bin);
560                 setObjectInputFilter.invoke(ois, filter);
561             } catch (IllegalAccessException | InvocationTargetException ex) {
562                 throw new StreamCorruptedException("Unable to set ObjectInputFilter on stream");
563             }
564         }
565         try {
566             return ois.readObject();
567         } finally {
568             ois.close();
569         }
570     }
571 
572     /**
573      * Calculate the next power of 2, greater than or equal to x.
574      * <p>
575      * From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
576      *
577      * @param x Value to round up
578      * @return The next power of 2 from x inclusive
579      */
580     private static int ceilingNextPowerOfTwo(final int x) {
581         final int BITS_PER_INT = 32;
582         return 1 << (BITS_PER_INT - Integer.numberOfLeadingZeros(x - 1));
583     }
584 
585     /**
586      * Reconstitute the {@code SortedArrayStringMap} instance from a stream (i.e.,
587      * deserialize it).
588      */
589     private void readObject(final java.io.ObjectInputStream s)  throws IOException, ClassNotFoundException {
590         if (!(s instanceof FilteredObjectInputStream) && setObjectInputFilter == null) {
591             throw new IllegalArgumentException("readObject requires a FilteredObjectInputStream or an ObjectInputStream that accepts an ObjectInputFilter");
592         }
593         // Read in the threshold (ignored), and any hidden stuff
594         s.defaultReadObject();
595 
596         // set other fields that need values
597         keys = EMPTY;
598         values = EMPTY;
599 
600         // Read in number of buckets
601         final int capacity = s.readInt();
602         if (capacity < 0) {
603             throw new InvalidObjectException("Illegal capacity: " + capacity);
604         }
605 
606         // Read number of mappings
607         final int mappings = s.readInt();
608         if (mappings < 0) {
609             throw new InvalidObjectException("Illegal mappings count: " + mappings);
610         }
611 
612         // allocate the bucket array;
613         if (mappings > 0) {
614             inflateTable(capacity);
615         } else {
616             threshold = capacity;
617         }
618 
619         // Read the keys and values, and put the mappings in the arrays
620         for (int i = 0; i < mappings; i++) {
621             keys[i] = (String) s.readObject();
622             try {
623                 final byte[] marshalledObject = (byte[]) s.readObject();
624                 values[i] = marshalledObject == null ? null : unmarshall(marshalledObject, s);
625             } catch (final Exception | LinkageError error) {
626                 handleSerializationException(error, i, keys[i]);
627                 values[i] = null;
628             }
629         }
630         size = mappings;
631     }
632 
633     private void handleSerializationException(final Throwable t, final int i, final String key) {
634         StatusLogger.getLogger().warn("Ignoring {} for key[{}] ('{}')", String.valueOf(t), i, keys[i]);
635     }
636 }