// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.common.io.protocol;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
/**
* A Map from primitive integers to Object values. This stores values
* for smaller keys in an Object array, and uses {@link Hashtable}
* only for larger keys. This is specifically designed to be used by
* J2me protocol buffer runtime ({@link ProtoBuf}, {@link ProtoBufType})
* to support large tags that are commonly used in Extensions/MessageSet.
*
* This class is not thread safe, so the client has to provide
* appropriate locking mechanism if the map is to be used from
* multiple threads.
*/
public class IntMap {
private static final int MAX_LOWER_BUFFER_SIZE = 64;
private static final int INITIAL_LOWER_BUFFER_SIZE = 8;
/**
* An iterator that returns int keys of the IntMap. IntMap has its
* own Iterator instead of Enumeration to avoid autoboxing. This
* uses the same buffer of the IntMap, so you should not update the
* IntMap while the iterator is in use. Once the IntMap is changed,
* the behavior of preiously obtained iterator is undefined, and a new
* KeyIterator has to be used instead.
*/
public class KeyIterator {
private int oneAheadIndex = 0;
private int currentKey = Integer.MIN_VALUE;
private Enumeration higherKeyEnumerator = null;
/**
* @returns true if there is more keys.
*/
public boolean hasNext() {
if (currentKey != Integer.MIN_VALUE) {
return true;
}
if (oneAheadIndex <= maxLowerKey) {
for (; oneAheadIndex <= maxLowerKey; oneAheadIndex++) {
if (lower[oneAheadIndex] != null) {
// record the key, then increment the oneAheadIndex.
currentKey = oneAheadIndex++;
return true;
}
}
}
if (higher != null) {
if (higherKeyEnumerator == null) {
higherKeyEnumerator = higher.keys();
}
if (higherKeyEnumerator.hasMoreElements()) {
Integer key = (Integer) higherKeyEnumerator.nextElement();
currentKey = key.intValue();
return true;
}
}
return false;
}
/**
* @returns next key
* @throws NoSuchElementException if there is no more keys.
*/
public int next() {
if (currentKey == Integer.MIN_VALUE && !hasNext()) {
throw new NoSuchElementException();
}
int key = currentKey;
currentKey = Integer.MIN_VALUE;
return key;
}
}
/** Stores values for lower keys */
private Object[] lower;
/** Hashtable for higher tags */
private Hashtable higher;
/** A maximum key that has been ever added to the lower buffer.*/
private int maxLowerKey;
/** A maximum key that has been ever added to the map.*/
private int maxKey;
/** the number of elements in lower buffer */
private int lowerCount;
/**
* Constructs an {@link IntMap} with default lower buffer size.
*/
public IntMap() {
this(INITIAL_LOWER_BUFFER_SIZE); // can expand 3 times
}
/**
* Constructs an {@link IntMap} with the suggested initial lower buffer size.
* The argument is just a hint and may not be used. If its value is
* larger than {@link MAX_LOWER_BUFFER_SIZE} or negative, it will use the
* MAX_LOWER_BUFFER_SIZE instead.
*/
IntMap(int initialLowerBufferSize) {
int lowerBufferSize = INITIAL_LOWER_BUFFER_SIZE;
if (initialLowerBufferSize > 0) {
lowerBufferSize = Math.min(initialLowerBufferSize, MAX_LOWER_BUFFER_SIZE);
}
lower = new Object[lowerBufferSize];
lowerCount = 0;
maxKey = Integer.MIN_VALUE;
maxLowerKey = Integer.MIN_VALUE;
}
/**
* A factory method to constructs an {@link IntMap} with the same
* lower buffer size.
*
* @return a new IntMap whose lower buffer size is same as
* this instance.
*/
public IntMap newIntMapWithSameBufferSize() {
return new IntMap(maxLowerKey);
}
/**
* @return the {@link KeyIterator} of the map.
*/
public KeyIterator keys() {
return new KeyIterator();
}
/**
* Returns max key that ever added to the map. Note that this is not
* max for current state. Removing the max key will not update the
* max key value. If nothing is added, it will return {@link
* Integer.MIN_VALUE}, which means you cannot tell if an value is
* added with MIN_VALUE key.
*/
public int maxKey() {
return maxKey;
}
/**
* Returns the number of key-value pairs in the map.
*/
public int size() {
return higher == null ? lowerCount : lowerCount + higher.size();
}
/**
* @return true if the map is empty.
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* Clears all key/value pairs. The map becomes empty after this
* operation, but does not release memory.
*/
public void clear() {
for (int i = 0; i < lower.length; i++) {
lower[i] = null;
}
if (higher != null) higher.clear();
maxKey = Integer.MIN_VALUE;
maxLowerKey = Integer.MIN_VALUE;
lowerCount = 0;
}
/**
* Returns the value associated with the given key.
*
* @param key a key
* @return the value associated with the given key. null if the map has
* no value for the key.
*/
public Object get(int key) {
if (key > maxKey) {
return null;
} else if (0 <= key && key <= maxLowerKey) {
return lower[key];
} else if (higher != null) {
return higher.get(key);
} else {
return null;
}
}
/**
* Maps the specified key to the given value in the {@link IntMap}.
* Caveat: Passing null value removes the value from the map. This is to
* keep the semantics used in {@link ProtoBuf}.
*
* @param key a key
* @param value the value to be added to the Map. If this is null,
* the key will be removed from the map. This method does nothing if
* the key does not exist and value is null.
*/
public void put(int key, Object value) {
if (value == null) {
remove(key);
return;
}
expandLowerIfNecessary(key);
maxKey = Math.max(key, maxKey);
if (0 <= key && key < lower.length) {
maxLowerKey = Math.max(key, maxLowerKey);
if (lower[key] == null) {
lowerCount++;
}
lower[key] = value;
} else {
if (higher == null) {
higher = new Hashtable();
}
higher.put(key, value);
}
}
/**
* Removes the key and corresponding value from the {@link IntMap}.
*
* @param key the key to remove. This method does nothing if the map does not
* have the value corresponding for the key.
* @return the removed value. null if the map does not have the value for
* the key.
*/
public Object remove(int key) {
Object deleted = null;
if (0 <= key && key < lower.length) {
deleted = lower[key];
if (deleted != null) {
lowerCount--;
}
lower[key] = null;
} else if (higher != null) {
return higher.remove(key);
}
return deleted;
}
/**
* Tests if given key has a corresponding value in this {@link IntMap}.
*
* @param key possible key.
* @return true
if the given key has the correspoding
* value. false
otherwise.
*/
public boolean containsKey(int key) {
if (0 <= key && key < lower.length) {
return lower[key] != null;
} else if (higher != null) {
return higher.containsKey(key);
}
return false;
}
/**
* Returns hashcode for {@link IntMap}.
*/
public int hashCode() {
int hashCode = 1;
for (int i = 0; i < lower.length ; i++) {
Object value = lower[i];
if (value != null) {
hashCode = 31 * hashCode + value.hashCode() + i;
}
}
// Hashtable in J2me does not implement hashCode(), so we simply
// use the size of hashtable.
return higher == null ? hashCode : hashCode + higher.size();
}
/**
* Compares the equality. Two IntMaps are considered equals iff
* both map have the same key-value pairs.
* Caveat: This assumes that the Class of each value object implements
* equals correctly. This may not be the case in J2me.
*
* @param object an object to be compared with
* @return true if the specified Object is equal to this IntMap
*/
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || !(object instanceof IntMap)) {
return false;
}
IntMap peer = (IntMap) object;
if (size() != peer.size()) {
return false;
}
return compareLowerBuffer(lower, peer.lower) &&
compareHashtable(higher, peer.higher);
}
private boolean compareLowerBuffer(Object[] lower1, Object[] lower2) {
int min = Math.min(lower1.length, lower2.length);
for (int i = 0; i < min; i++) {
if ((lower1[i] == null && lower2[i] != null) ||
(lower1[i] != null && !lower1[i].equals(lower2[i]))) {
return false;
}
}
// make sure there are no values in remaining fields.
if (lower1.length > lower2.length) {
for (int i = min; i < lower1.length; i++) {
if (lower1[i] != null) return false;
}
} else if (lower1.length < lower2.length) {
for (int i = min; i < lower2.length; i++) {
if (lower2[i] != null) return false;
}
}
return true;
}
/**
* J2me's Hashtable does not implement equal, Bummer!
*/
private static boolean compareHashtable(Hashtable h1, Hashtable h2) {
if (h1 == h2) { // null == null is caught here
return true;
}
if (h1 == null || h2 == null) {
return false;
}
if (h1.size() != h2.size()) {
return false;
}
// Ensure the values are the same.
Enumeration h1Keys = h1.keys();
while (h1Keys.hasMoreElements()) {
Object key = h1Keys.nextElement();
Object h1Value = h1.get(key);
Object h2Value = h2.get(key);
if (!h1Value.equals(h2Value)) {
return false;
}
}
return true;
}
/**
* Expands lower buffer iff the key does not fit to current buffer size,
* but will fit in MAX buffer size.
*/
private void expandLowerIfNecessary(int key) {
if (key <= MAX_LOWER_BUFFER_SIZE && key >= lower.length && key > 0) {
int size = lower.length;
do {
size <<= 1;
} while (size <= key);
size = Math.min(size, MAX_LOWER_BUFFER_SIZE);
Object[] newLower = new Object[size];
System.arraycopy(lower, 0, newLower, 0, lower.length);
lower = newLower;
}
}
/* {@inheritDoc} */
public String toString() {
StringBuffer buffer = new StringBuffer("IntMap{lower:");
for (int i = 0; i < lower.length; i++) {
if (lower[i] != null) {
buffer.append(i);
buffer.append("=>");
buffer.append(lower[i]);
buffer.append(", ");
}
}
buffer.append(", higher:" + higher + "}");
return buffer.toString();
}
}