diff --git a/src/com/google/common/io/protocol/IntMap.java b/src/com/google/common/io/protocol/IntMap.java
new file mode 100644
index 000000000..9f0b8fc34
--- /dev/null
+++ b/src/com/google/common/io/protocol/IntMap.java
@@ -0,0 +1,383 @@
+// 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();
+ }
+}
diff --git a/src/com/google/common/io/protocol/ProtoBuf.java b/src/com/google/common/io/protocol/ProtoBuf.java
index 2a2f8b773..ae7e4a6d7 100644
--- a/src/com/google/common/io/protocol/ProtoBuf.java
+++ b/src/com/google/common/io/protocol/ProtoBuf.java
@@ -7,9 +7,7 @@ import java.io.*;
import java.util.*;
/**
- * Protocol buffer message object. Currently, it is assumed that tags ids are
- * not large. This could be improved by storing a start offset, reducing the
- * assumption to a dense number space.
+ * Protocol buffer message object.
*
* ProtoBuf instances may or may not reference a ProtoBufType instance,
* representing information from a corresponding .proto file, which defines tag
@@ -33,7 +31,6 @@ import java.util.*;
* this behavior is that default values cannot be removed -- they would reappear
* after a serialization cycle. If a tag has repeated values, setXXX(tag, value)
* will overwrite all of them and getXXX(tag) will throw an exception.
- *
*/
public class ProtoBuf {
@@ -45,7 +42,9 @@ public class ProtoBuf {
private static final String MSG_MISMATCH = "Type mismatch";
private static final String MSG_UNSUPPORTED = "Unsupp.Type";
- // names copied from //net/proto2/internal/wire_format.cc
+ // see
+ // http://code.google.com/apis/protocolbuffers/docs/overview.html
+ // for more details about wire format.
static final int WIRETYPE_END_GROUP = 4;
static final int WIRETYPE_FIXED32 = 5;
static final int WIRETYPE_FIXED64 = 1;
@@ -56,20 +55,19 @@ public class ProtoBuf {
/** Maximum number of bytes for VARINT wire format (64 bit, 7 bit/byte) */
private static final int VARINT_MAX_BYTES = 10;
- private static Long[] SMALL_NUMBERS = {
- new Long(0), new Long(1), new Long(2), new Long(3), new Long(4),
- new Long(5), new Long(6), new Long(7), new Long(8), new Long(9),
- new Long(10), new Long(11), new Long(12), new Long(13), new Long(14),
- new Long(15)};
-
private ProtoBufType msgType;
- private final Vector values = new Vector();
+ private final IntMap values;
/**
* Wire types picked up on the wire or implied by setters (if no other
* type information is available.
*/
- private final StringBuffer wireTypes = new StringBuffer();
+ private final IntMap wireTypes;
+
+ /**
+ * Saved by a call to #getCachedDataSize(false) and returned in #getCachedSize()
+ */
+ private int cachedSize = Integer.MIN_VALUE;
/**
* Creates a protocol message according to the given description. The
@@ -78,14 +76,22 @@ public class ProtoBuf {
*/
public ProtoBuf(ProtoBufType type) {
this.msgType = type;
+ if (type != null) {
+ // if the type is known, use the type to create IntMaps.
+ values = type.newIntMapForProtoBuf();
+ wireTypes = type.newIntMapForProtoBuf();
+ } else {
+ values = new IntMap();
+ wireTypes = new IntMap();
+ }
}
- /**
+ /**
* Clears all data stored in this ProtoBuf.
*/
public void clear() {
- values.setSize(0);
- wireTypes.setLength(0);
+ values.clear();
+ wireTypes.clear();
}
/**
@@ -210,7 +216,7 @@ public class ProtoBuf {
return (int) ((Long) getObject(tag, ProtoBufType.TYPE_INT32)).longValue();
}
- /**
+ /**
* Returns the integer value for the given repeated tag at the given index.
*/
public int getInt(int tag, int index) {
@@ -305,7 +311,8 @@ public class ProtoBuf {
* @param type the new type
*/
void setType(ProtoBufType type) {
- if (values.size() != 0 ||
+ // reject if the type is already set, or value is alreay set.
+ if (!values.isEmpty() ||
(msgType != null && type != null && type != msgType)) {
throw new IllegalArgumentException();
}
@@ -359,7 +366,8 @@ public class ProtoBuf {
* @return this
* @throws IOException raised if an IO exception occurs in the
* underlying stream or the end of the stream is reached at
- * an unexpected position
+ * an unexpected position, or if we encounter bad data
+ * while reading.
*/
public int parse(InputStream is, int available) throws IOException {
@@ -376,11 +384,7 @@ public class ProtoBuf {
break;
}
int tag = (int) (tagAndType >>> 3);
- while (wireTypes.length() <= tag){
- wireTypes.append((char) ProtoBufType.TYPE_UNDEFINED);
- }
- wireTypes.setCharAt(tag, (char) wireType);
-
+ wireTypes.put(tag, wireType);
// first step: decode tag value
Object value;
switch (wireType) {
@@ -390,8 +394,7 @@ public class ProtoBuf {
if (isZigZagEncodedType(tag)) {
v = zigZagDecode(v);
}
- value = (v >= 0 && v < SMALL_NUMBERS.length) ?
- SMALL_NUMBERS[(int) v] : new Long(v);
+ value = v;
break;
// also used for fixed values
@@ -408,9 +411,7 @@ public class ProtoBuf {
shift += 8;
}
- value = (v >= 0 && v < SMALL_NUMBERS.length)
- ? SMALL_NUMBERS[(int) v]
- : new Long(v);
+ value = v;
break;
case WIRETYPE_LENGTH_DELIMITED:
@@ -445,7 +446,8 @@ public class ProtoBuf {
break;
default:
- throw new RuntimeException(MSG_UNSUPPORTED + wireType);
+ throw new IOException("Unknown wire type " + wireType +
+ ", reading garbage data?");
}
insertObject(tag, getCount(tag), value);
}
@@ -466,9 +468,9 @@ public class ProtoBuf {
throw new ArrayIndexOutOfBoundsException();
}
if (count == 1){
- values.setElementAt(null, tag);
+ values.remove(tag);
} else {
- Vector v = (Vector) values.elementAt(tag);
+ Vector v = (Vector) values.get(tag);
v.removeElementAt(index);
}
}
@@ -477,12 +479,15 @@ public class ProtoBuf {
* Returns the number of repeated and optional (0..1) values for a given tag.
* Note: Default values are not counted (and in general not considered in
* access methods for repeated tags), but considered for has(tag).
+ *
+ * @param tag the tag of the field
+ * @throws ArrayIndexOutOfBoundsException when tag is < 0
*/
public int getCount(int tag) {
- if (tag >= values.size()){
- return 0;
+ if (tag < 0) {
+ throw new ArrayIndexOutOfBoundsException(tag);
}
- Object o = values.elementAt(tag);
+ Object o = values.get(tag);
if (o == null){
return 0;
}
@@ -502,38 +507,69 @@ public class ProtoBuf {
tagType = msgType.getType(tag);
}
- if (tagType == ProtoBufType.TYPE_UNDEFINED && tag < wireTypes.length()) {
- tagType = wireTypes.charAt(tag);
+ if (tagType == ProtoBufType.TYPE_UNDEFINED) {
+ Integer tagTypeObj = (Integer) wireTypes.get(tag);
+ if (tagTypeObj != null) {
+ tagType = tagTypeObj.intValue();
+ }
}
-
+
if (tagType == ProtoBufType.TYPE_UNDEFINED && getCount(tag) > 0) {
Object o = getObject(tag, 0, ProtoBufType.TYPE_UNDEFINED);
-
- tagType = (o instanceof Long) || (o instanceof Boolean)
+
+ tagType = (o instanceof Long) || (o instanceof Boolean)
? WIRETYPE_VARINT : WIRETYPE_LENGTH_DELIMITED;
}
-
+
return tagType;
}
-
+
/**
* Returns the number of bytes needed to store this protocol buffer
*/
public int getDataSize() {
- int size = 0;
- for (int tag = 0; tag <= maxTag(); tag++) {
- for (int i = 0; i < getCount(tag); i++) {
- size += getDataSize(tag, i);
- }
- }
- return size;
+ return getCachedDataSize(false /* don't trust cache */);
}
-
-
- /**
- * Returns the size of the given value
+
+ /**
+ * Each Protobuf keeps track of a cachedSize
that is
+ * used to short circuit evaluation of its children's sizes. This value
+ * should only be trusted if you are reasonably certain it cannot be
+ * corrupt. (A corrupt cache can happen if any child ProtoBuf of this
+ * ProtoBuf has had a value set. In general, it is best to only trust
+ * the cache if you have just finished cleansing it.)
+ *
+ *