001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.util;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InvalidObjectException;
023import java.io.ObjectInputStream;
024import java.io.ObjectOutputStream;
025import java.io.StreamCorruptedException;
026import java.lang.reflect.InvocationTargetException;
027import java.lang.reflect.Method;
028import java.lang.reflect.Modifier;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.ConcurrentModificationException;
032import java.util.HashMap;
033import java.util.Map;
034import java.util.Objects;
035
036import org.apache.logging.log4j.status.StatusLogger;
037
038/**
039 * <em>Consider this class private.</em>
040 * Array-based implementation of the {@code ReadOnlyStringMap} interface. Keys are held in a sorted array.
041 * <p>
042 * This is not a generic collection, but makes some trade-offs to optimize for the Log4j context data use case:
043 * </p>
044 * <ul>
045 *   <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li>
046 *   <li>Fast copy. If the ThreadContextMap is also an instance of {@code SortedArrayStringMap}, the full thread context
047 *     data can be transferred with two array copies and two field updates.</li>
048 *   <li>Acceptable performance for small data sets. The current implementation stores keys in a sorted array, values
049 *     are stored in a separate array at the same index.
050 *     Worst-case performance of {@code get} and {@code containsKey} is O(log N),
051 *     worst-case performance of {@code put} and {@code remove} is O(N log N).
052 *     The expectation is that for the small values of {@code N} (less than 100) that are the vast majority of
053 *     ThreadContext use cases, the constants dominate performance more than the asymptotic performance of the
054 *     algorithms used.
055 *     </li>
056 *     <li>Compact representation.</li>
057 * </ul>
058 *
059 * @since 2.7
060 */
061public class SortedArrayStringMap implements IndexedStringMap {
062
063    /**
064     * The default initial capacity.
065     */
066    private static final int DEFAULT_INITIAL_CAPACITY = 4;
067    private static final long serialVersionUID = -5748905872274478116L;
068    private static final int HASHVAL = 31;
069
070    private static final TriConsumer<String, Object, StringMap> PUT_ALL = (key, value, contextData) -> contextData.putValue(key, value);
071
072    /**
073     * An empty array instance to share when the table is not inflated.
074     */
075    private static final String[] EMPTY = {};
076    private static final String FROZEN = "Frozen collection cannot be modified";
077
078    private transient String[] keys = EMPTY;
079    private transient Object[] values = EMPTY;
080
081    /**
082     * The number of key-value mappings contained in this map.
083     */
084    private transient int size;
085
086    private static final Method setObjectInputFilter;
087    private static final Method getObjectInputFilter;
088    private static final Method newObjectInputFilter;
089
090    static {
091        Method[] methods = ObjectInputStream.class.getMethods();
092        Method setMethod = null;
093        Method getMethod = null;
094        for (final Method method : methods) {
095            if (method.getName().equals("setObjectInputFilter")) {
096                setMethod = method;
097            } else if (method.getName().equals("getObjectInputFilter")) {
098                getMethod = method;
099            }
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}