blob: c7df7a73a738876e26e2cc39895283c9fe3c5e64 [file] [log] [blame]
/*
* 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.logging.log4j.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.logging.log4j.status.StatusLogger;
/**
* <em>Consider this class private.</em>
* Array-based implementation of the {@code ReadOnlyStringMap} interface. Keys are held in a sorted array.
* <p>
* This is not a generic collection, but makes some trade-offs to optimize for the Log4j context data use case:
* </p>
* <ul>
* <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li>
* <li>Fast copy. If the ThreadContextMap is also an instance of {@code SortedArrayStringMap}, the full thread context
* data can be transferred with two array copies and two field updates.</li>
* <li>Acceptable performance for small data sets. The current implementation stores keys in a sorted array, values
* are stored in a separate array at the same index.
* Worst-case performance of {@code get} and {@code containsKey} is O(log N),
* worst-case performance of {@code put} and {@code remove} is O(N log N).
* The expectation is that for the small values of {@code N} (less than 100) that are the vast majority of
* ThreadContext use cases, the constants dominate performance more than the asymptotic performance of the
* algorithms used.
* </li>
* <li>Compact representation.</li>
* </ul>
*
* @since 2.7
*/
public class SortedArrayStringMap implements IndexedStringMap {
/**
* The default initial capacity.
*/
private static final int DEFAULT_INITIAL_CAPACITY = 4;
private static final long serialVersionUID = -5748905872274478116L;
private static final int HASHVAL = 31;
private static final TriConsumer<String, Object, StringMap> PUT_ALL = (key, value, contextData) -> contextData.putValue(key, value);
/**
* An empty array instance to share when the table is not inflated.
*/
private static final String[] EMPTY = {};
private static final String FROZEN = "Frozen collection cannot be modified";
private transient String[] keys = EMPTY;
private transient Object[] values = EMPTY;
/**
* The number of key-value mappings contained in this map.
*/
private transient int size;
private static final Method setObjectInputFilter;
private static final Method getObjectInputFilter;
private static final Method newObjectInputFilter;
static {
Method[] methods = ObjectInputStream.class.getMethods();
Method setMethod = null;
Method getMethod = null;
for (final Method method : methods) {
if (method.getName().equals("setObjectInputFilter")) {
setMethod = method;
} else if (method.getName().equals("getObjectInputFilter")) {
getMethod = method;
}
}
Method newMethod = null;
try {
if (setMethod != null) {
final Class<?> clazz = Class.forName("org.apache.logging.log4j.internal.DefaultObjectInputFilter");
methods = clazz.getMethods();
for (final Method method : methods) {
if (method.getName().equals("newInstance") && Modifier.isStatic(method.getModifiers())) {
newMethod = method;
break;
}
}
}
} catch (final ClassNotFoundException ex) {
// Ignore the exception
}
newObjectInputFilter = newMethod;
setObjectInputFilter = setMethod;
getObjectInputFilter = getMethod;
}
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
private int threshold;
private boolean immutable;
private transient boolean iterating;
public SortedArrayStringMap() {
this(DEFAULT_INITIAL_CAPACITY);
}
public SortedArrayStringMap(final int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("Initial capacity must be at least zero but was " + initialCapacity);
}
threshold = ceilingNextPowerOfTwo(initialCapacity == 0 ? 1 : initialCapacity);
}
public SortedArrayStringMap(final ReadOnlyStringMap other) {
if (other instanceof SortedArrayStringMap) {
initFrom0((SortedArrayStringMap) other);
} else if (other != null) {
resize(ceilingNextPowerOfTwo(other.size()));
other.forEach(PUT_ALL, this);
}
}
public SortedArrayStringMap(final Map<String, ?> map) {
resize(ceilingNextPowerOfTwo(map.size()));
for (final Map.Entry<String, ?> entry : map.entrySet()) {
putValue(entry.getKey(), entry.getValue());
}
}
/**
* For unit testing.
* @return The threshold.
*/
public int getThreshold() {
return threshold;
}
private void assertNotFrozen() {
if (immutable) {
throw new UnsupportedOperationException(FROZEN);
}
}
private void assertNoConcurrentModification() {
if (iterating) {
throw new ConcurrentModificationException();
}
}
@Override
public void clear() {
if (keys == EMPTY) {
return;
}
assertNotFrozen();
assertNoConcurrentModification();
Arrays.fill(keys, 0, size, null);
Arrays.fill(values, 0, size, null);
size = 0;
}
@Override
public boolean containsKey(final String key) {
return indexOfKey(key) >= 0;
}
@Override
public Map<String, String> toMap() {
final Map<String, String> result = new HashMap<>(size());
for (int i = 0; i < size(); i++) {
final Object value = getValueAt(i);
result.put(getKeyAt(i), value == null ? null : String.valueOf(value));
}
return result;
}
@Override
public void freeze() {
immutable = true;
}
@Override
public boolean isFrozen() {
return immutable;
}
@SuppressWarnings("unchecked")
@Override
public <V> V getValue(final String key) {
final int index = indexOfKey(key);
if (index < 0) {
return null;
}
return (V) values[index];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public int indexOfKey(final String key) {
if (keys == EMPTY) {
return -1;
}
if (key == null) { // null key is located at the start of the array
return nullKeyIndex(); // insert at index zero
}
final int start = size > 0 && keys[0] == null ? 1 : 0;
return Arrays.binarySearch(keys, start, size, key);
}
private int nullKeyIndex() {
return size > 0 && keys[0] == null ? 0 : ~0;
}
@Override
public void putValue(final String key, final Object value) {
assertNotFrozen();
assertNoConcurrentModification();
if (keys == EMPTY) {
inflateTable(threshold);
}
final int index = indexOfKey(key);
if (index >= 0) {
keys[index] = key;
values[index] = value;
} else { // not found, so insert.
insertAt(~index, key, value);
}
}
private void insertAt(final int index, final String key, final Object value) {
ensureCapacity();
System.arraycopy(keys, index, keys, index + 1, size - index);
System.arraycopy(values, index, values, index + 1, size - index);
keys[index] = key;
values[index] = value;
size++;
}
@Override
public void putAll(final ReadOnlyStringMap source) {
if (source == this || source == null || source.isEmpty()) {
// this.putAll(this) does not modify this collection
// this.putAll(null) does not modify this collection
// this.putAll(empty ReadOnlyStringMap) does not modify this collection
return;
}
assertNotFrozen();
assertNoConcurrentModification();
if (source instanceof SortedArrayStringMap) {
if (this.size == 0) {
initFrom0((SortedArrayStringMap) source);
} else {
merge((SortedArrayStringMap) source);
}
} else if (source != null) {
source.forEach(PUT_ALL, this);
}
}
private void initFrom0(final SortedArrayStringMap other) {
if (keys.length < other.size) {
keys = new String[other.threshold];
values = new Object[other.threshold];
}
System.arraycopy(other.keys, 0, keys, 0, other.size);
System.arraycopy(other.values, 0, values, 0, other.size);
size = other.size;
threshold = other.threshold;
}
private void merge(final SortedArrayStringMap other) {
final String[] myKeys = keys;
final Object[] myVals = values;
final int newSize = other.size + this.size;
threshold = ceilingNextPowerOfTwo(newSize);
if (keys.length < threshold) {
keys = new String[threshold];
values = new Object[threshold];
}
// move largest collection to the beginning of this data structure, smallest to the end
boolean overwrite = true;
if (other.size() > size()) {
// move original key-values to end
System.arraycopy(myKeys, 0, keys, other.size, this.size);
System.arraycopy(myVals, 0, values, other.size, this.size);
// insert additional key-value pairs at the beginning
System.arraycopy(other.keys, 0, keys, 0, other.size);
System.arraycopy(other.values, 0, values, 0, other.size);
size = other.size;
// loop over original keys and insert (drop values for same key)
overwrite = false;
} else {
System.arraycopy(myKeys, 0, keys, 0, this.size);
System.arraycopy(myVals, 0, values, 0, this.size);
// move additional key-value pairs to end
System.arraycopy(other.keys, 0, keys, this.size, other.size);
System.arraycopy(other.values, 0, values, this.size, other.size);
// new values are at the end, will be processed below. Overwrite is true.
}
for (int i = size; i < newSize; i++) {
final int index = indexOfKey(keys[i]);
if (index < 0) { // key does not exist
insertAt(~index, keys[i], values[i]);
} else if (overwrite) { // existing key: only overwrite when looping over the new values
keys[index] = keys[i];
values[index] = values[i];
}
}
// prevent memory leak: null out references
Arrays.fill(keys, size, newSize, null);
Arrays.fill(values, size, newSize, null);
}
private void ensureCapacity() {
if (size >= threshold) {
resize(threshold * 2);
}
}
private void resize(final int newCapacity) {
final String[] oldKeys = keys;
final Object[] oldValues = values;
keys = new String[newCapacity];
values = new Object[newCapacity];
System.arraycopy(oldKeys, 0, keys, 0, size);
System.arraycopy(oldValues, 0, values, 0, size);
threshold = newCapacity;
}
/**
* Inflates the table.
*/
private void inflateTable(final int toSize) {
threshold = toSize;
keys = new String[toSize];
values = new Object[toSize];
}
@Override
public void remove(final String key) {
if (keys == EMPTY) {
return;
}
final int index = indexOfKey(key);
if (index >= 0) {
assertNotFrozen();
assertNoConcurrentModification();
System.arraycopy(keys, index + 1, keys, index, size - 1 - index);
System.arraycopy(values, index + 1, values, index, size - 1 - index);
keys[size - 1] = null;
values[size - 1] = null;
size--;
}
}
@Override
public String getKeyAt(final int index) {
if (index < 0 || index >= size) {
return null;
}
return keys[index];
}
@SuppressWarnings("unchecked")
@Override
public <V> V getValueAt(final int index) {
if (index < 0 || index >= size) {
return null;
}
return (V) values[index];
}
@Override
public int size() {
return size;
}
@SuppressWarnings("unchecked")
@Override
public <V> void forEach(final BiConsumer<String, ? super V> action) {
iterating = true;
try {
for (int i = 0; i < size; i++) {
action.accept(keys[i], (V) values[i]);
}
} finally {
iterating = false;
}
}
@SuppressWarnings("unchecked")
@Override
public <V, T> void forEach(final TriConsumer<String, ? super V, T> action, final T state) {
iterating = true;
try {
for (int i = 0; i < size; i++) {
action.accept(keys[i], (V) values[i], state);
}
} finally {
iterating = false;
}
}
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof SortedArrayStringMap)) {
return false;
}
final SortedArrayStringMap other = (SortedArrayStringMap) obj;
if (this.size() != other.size()) {
return false;
}
for (int i = 0; i < size(); i++) {
if (!Objects.equals(keys[i], other.keys[i])) {
return false;
}
if (!Objects.equals(values[i], other.values[i])) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int result = 37;
result = HASHVAL * result + size;
result = HASHVAL * result + hashCode(keys, size);
result = HASHVAL * result + hashCode(values, size);
return result;
}
private static int hashCode(final Object[] values, final int length) {
int result = 1;
for (int i = 0; i < length; i++) {
result = HASHVAL * result + (values[i] == null ? 0 : values[i].hashCode());
}
return result;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(256);
sb.append('{');
for (int i = 0; i < size; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(keys[i]).append('=');
sb.append(values[i] == this ? "(this map)" : values[i]);
}
sb.append('}');
return sb.toString();
}
/**
* Save the state of the {@code SortedArrayStringMap} instance to a stream (i.e.,
* serialize it).
*
* @param s The ObjectOutputStream.
* @throws IOException if there is an error serializing the object to the stream.
*
* @serialData The <i>capacity</i> of the SortedArrayStringMap (the length of the
* bucket array) is emitted (int), followed by the
* <i>size</i> (an int, the number of key-value
* mappings), followed by the key (Object) and value (Object)
* for each key-value mapping. The key-value mappings are
* emitted in no particular order.
*/
private void writeObject(final java.io.ObjectOutputStream s) throws IOException {
// Write out the threshold, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
if (keys == EMPTY) {
s.writeInt(ceilingNextPowerOfTwo(threshold));
} else {
s.writeInt(keys.length);
}
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (size > 0) {
for (int i = 0; i < size; i++) {
s.writeObject(keys[i]);
try {
s.writeObject(marshall(values[i]));
} catch (final Exception e) {
handleSerializationException(e, i, keys[i]);
s.writeObject(null);
}
}
}
}
private static byte[] marshall(final Object obj) throws IOException {
if (obj == null) {
return null;
}
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (final ObjectOutputStream oos = new ObjectOutputStream(bout)) {
oos.writeObject(obj);
oos.flush();
return bout.toByteArray();
}
}
@SuppressWarnings("BanSerializableRead")
private static Object unmarshall(final byte[] data, final ObjectInputStream inputStream)
throws IOException, ClassNotFoundException {
final ByteArrayInputStream bin = new ByteArrayInputStream(data);
Collection<String> allowedClasses = null;
final ObjectInputStream ois;
if (inputStream instanceof FilteredObjectInputStream) {
allowedClasses = ((FilteredObjectInputStream) inputStream).getAllowedClasses();
ois = new FilteredObjectInputStream(bin, allowedClasses);
} else {
try {
final Object obj = getObjectInputFilter.invoke(inputStream);
final Object filter = newObjectInputFilter.invoke(null, obj);
ois = new ObjectInputStream(bin);
setObjectInputFilter.invoke(ois, filter);
} catch (final IllegalAccessException | InvocationTargetException ex) {
throw new StreamCorruptedException("Unable to set ObjectInputFilter on stream");
}
}
try {
return ois.readObject();
} finally {
ois.close();
}
}
/**
* Calculate the next power of 2, greater than or equal to x.
* <p>
* From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
*
* @param x Value to round up
* @return The next power of 2 from x inclusive
*/
private static int ceilingNextPowerOfTwo(final int x) {
final int BITS_PER_INT = 32;
return 1 << (BITS_PER_INT - Integer.numberOfLeadingZeros(x - 1));
}
/**
* Reconstitute the {@code SortedArrayStringMap} instance from a stream (i.e.,
* deserialize it).
* @param s The ObjectInputStream.
* @throws IOException If there is an error reading the input stream.
* @throws ClassNotFoundException if the class to be instantiated could not be found.
*/
private void readObject(final java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
if (!(s instanceof FilteredObjectInputStream) && setObjectInputFilter == null) {
throw new IllegalArgumentException("readObject requires a FilteredObjectInputStream or an ObjectInputStream that accepts an ObjectInputFilter");
}
// Read in the threshold (ignored), and any hidden stuff
s.defaultReadObject();
// set other fields that need values
keys = EMPTY;
values = EMPTY;
// Read in number of buckets
final int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " + capacity);
}
// Read number of mappings
final int mappings = s.readInt();
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
}
// allocate the bucket array;
if (mappings > 0) {
inflateTable(capacity);
} else {
threshold = capacity;
}
// Read the keys and values, and put the mappings in the arrays
for (int i = 0; i < mappings; i++) {
keys[i] = (String) s.readObject();
try {
final byte[] marshalledObject = (byte[]) s.readObject();
values[i] = marshalledObject == null ? null : unmarshall(marshalledObject, s);
} catch (final Exception | LinkageError error) {
handleSerializationException(error, i, keys[i]);
values[i] = null;
}
}
size = mappings;
}
private void handleSerializationException(final Throwable t, final int i, final String key) {
StatusLogger.getLogger().warn("Ignoring {} for key[{}] ('{}')", String.valueOf(t), i, keys[i]);
}
}