/* * 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.pdfbox.util; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Map implementation with a smallest possible memory usage. * It should only be used for maps with small number of items * (e.g. <30) since most operations have an O(n) complexity. * Thus it should be used in cases with large number of map * objects, each having only few items. * * <p><code>null</code> is not supported for keys or values.</p> */ public class SmallMap<K, V> implements Map<K, V> { /** * stores key-value pair as 2 objects; key first; in case of empty map this might be <code>null</code> */ private Object[] mapArr; /** Creates empty map. */ public SmallMap() { } /** Creates map filled with entries from provided map. */ public SmallMap(Map<? extends K, ? extends V> initMap) { putAll(initMap); } /** * Returns index of key within map-array or <code>-1</code> * if key is not found (or key is <code>null</code>). */ private int findKey(Object key) { if (isEmpty() || (key==null)) { return -1; } for ( int aIdx = 0; aIdx < mapArr.length; aIdx+=2 ) { if (key.equals(mapArr[aIdx])) { return aIdx; } } return -1; } /** * Returns index of value within map-array or <code>-1</code> * if value is not found (or value is <code>null</code>). */ private int findValue(Object value) { if (isEmpty() || (value==null)) { return -1; } for ( int aIdx = 1; aIdx < mapArr.length; aIdx+=2 ) { if (value.equals(mapArr[aIdx])) { return aIdx; } } return -1; } @Override public int size() { return mapArr == null ? 0 : mapArr.length >> 1; } @Override public boolean isEmpty() { return (mapArr == null) || (mapArr.length == 0); } @Override public boolean containsKey(Object key) { return findKey(key) >= 0; } @Override public boolean containsValue(Object value) { return findValue(value) >= 0; } @SuppressWarnings("unchecked") @Override public V get(Object key) { int kIdx = findKey(key); return kIdx < 0 ? null : (V) mapArr[kIdx+1]; } @Override public V put(K key, V value) { if ((key == null) || (value == null)) { throw new NullPointerException( "Key or value must not be null."); } if (mapArr == null) { mapArr = new Object[] { key, value }; return null; } else { int kIdx = findKey(key); if (kIdx < 0) { // key unknown int oldLen = mapArr.length; Object[] newMapArr = new Object[oldLen+2]; System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); newMapArr[oldLen] = key; newMapArr[oldLen+1] = value; mapArr = newMapArr; return null; } else { // key exists; replace value @SuppressWarnings("unchecked") V oldValue = (V) mapArr[kIdx+1]; mapArr[kIdx+1] = value; return oldValue; } } } @Override public V remove(Object key) { int kIdx = findKey(key); if (kIdx < 0) { // not found return null; } @SuppressWarnings("unchecked") V oldValue = (V) mapArr[kIdx+1]; int oldLen = mapArr.length; if (oldLen == 2) { // was last entry mapArr = null; } else { Object[] newMapArr = new Object[oldLen-2]; System.arraycopy(mapArr, 0, newMapArr, 0, kIdx); System.arraycopy(mapArr, kIdx+2, newMapArr, kIdx, oldLen - kIdx - 2); mapArr = newMapArr; } return oldValue; } @Override public final void putAll(Map<? extends K, ? extends V> otherMap) { if ((mapArr == null) || (mapArr.length == 0)) { // existing map is empty mapArr = new Object[otherMap.size() << 1]; int aIdx = 0; for (Entry<? extends K, ? extends V> entry : otherMap.entrySet()) { if ((entry.getKey() == null) || (entry.getValue() == null)) { throw new NullPointerException( "Key or value must not be null."); } mapArr[aIdx++] = entry.getKey(); mapArr[aIdx++] = entry.getValue(); } } else { int oldLen = mapArr.length; // first increase array size to hold all to put entries as if they have unknown keys // reduce after adding all to the required size Object[] newMapArr = new Object[oldLen+(otherMap.size() << 1)]; System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); int newIdx = oldLen; for (Entry<? extends K, ? extends V> entry : otherMap.entrySet()) { if ((entry.getKey() == null) || (entry.getValue() == null)) { throw new NullPointerException( "Key or value must not be null."); } int existKeyIdx = findKey(entry.getKey()); if (existKeyIdx >= 0) { // existing key newMapArr[existKeyIdx+1] = entry.getValue(); } else { // new key newMapArr[newIdx++] = entry.getKey(); newMapArr[newIdx++] = entry.getValue(); } } if (newIdx < newMapArr.length) { Object[] reducedMapArr = new Object[newIdx]; System.arraycopy(newMapArr, 0, reducedMapArr, 0, newIdx); newMapArr = reducedMapArr; } mapArr = newMapArr; } } @Override public void clear() { mapArr = null; } /** * Returns a set view of the keys contained in this map. * * <p>The current implementation does not allow changes to the * returned key set (which would have to be reflected in the * underlying map.</p> */ @SuppressWarnings("unchecked") @Override public Set<K> keySet() { if (isEmpty()) { return Collections.emptySet(); } Set<K> keys = new LinkedHashSet<>(); for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) { keys.add((K)mapArr[kIdx]); } return Collections.unmodifiableSet( keys ); } /** * Returns a collection of the values contained in this map. * * <p>The current implementation does not allow changes to the * returned collection (which would have to be reflected in the * underlying map.</p> */ @SuppressWarnings("unchecked") @Override public Collection<V> values() { if (isEmpty()) { return Collections.emptySet(); } List<V> values = new ArrayList<>(mapArr.length >> 1); for (int vIdx = 1; vIdx < mapArr.length; vIdx+=2) { values.add((V)mapArr[vIdx]); } return Collections.unmodifiableList( values ); } private class SmallMapEntry implements Entry<K, V> { private final int keyIdx; SmallMapEntry(int keyInMapIdx) { keyIdx = keyInMapIdx; } @SuppressWarnings("unchecked") @Override public K getKey() { return (K)mapArr[keyIdx]; } @SuppressWarnings("unchecked") @Override public V getValue() { return (V)mapArr[keyIdx+1]; } @Override public V setValue(V value) { if (value == null) { throw new NullPointerException( "Key or value must not be null."); } V oldValue = getValue(); mapArr[keyIdx+1] = value; return oldValue; } @Override public int hashCode() { return getKey().hashCode(); } @Override public boolean equals(Object obj) { if (!(obj instanceof SmallMap.SmallMapEntry)) { return false; } @SuppressWarnings("unchecked") SmallMapEntry other = (SmallMapEntry) obj; return getKey().equals(other.getKey()) && getValue().equals(other.getValue()); } } @Override public Set<java.util.Map.Entry<K, V>> entrySet() { if (isEmpty()) { return Collections.emptySet(); } Set<java.util.Map.Entry<K, V>> entries = new LinkedHashSet<>(); for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) { entries.add(new SmallMapEntry(kIdx)); } return Collections.unmodifiableSet( entries ); } }