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}