// Copyright 2007 The Android Open Source Project
// All Rights Reserved.
package com.google.common.io.protocol;
import java.io.*;
import java.util.*;
/**
* Protocol buffer message object.
* <p>
* ProtoBuf instances may or may not reference a ProtoBufType instance,
* representing information from a corresponding .proto file, which defines tag
* data types. The type can only be set in the constructor, it cannot be
* changed later.
* <p>
* If the type is null, the ProtoBuffer should be used only for reading or
* as a local persistent storage buffer. An untyped Protocol Buffer must never
* be sent to a server.
* <p>
* If a ProtoBufType is set, unknown values are read from the stream and
* preserved, but it is not possible to add values for undefined tags using
* this API. Attempts to set undefined tags will result in an exception.
* <p>
* This class provides two different sets of access methods for simple and
* repeated tags. Simple access methods are has(tag), getXXX(tag),
* and setXXX(tag, value). Access methods for repeated tags are getCount(tag),
* getXXX(tag, index), remove(tag, index), insert(tag, index, value) and
* addXXX(tag, value). Note that both sets of methods can be used in both cases,
* but only the simple methods take default values into account. The reason for
* 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 {
public static final Boolean FALSE = new Boolean(false);
public static final Boolean TRUE = new Boolean(true);
private static final String MSG_EOF = "Unexp.EOF";
private static final String MSG_MISMATCH = "Type mismatch";
private static final String MSG_UNSUPPORTED = "Unsupp.Type";
// 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;
static final int WIRETYPE_LENGTH_DELIMITED = 2;
static final int WIRETYPE_START_GROUP = 3;
static final int WIRETYPE_VARINT = 0;
/** Maximum number of bytes for VARINT wire format (64 bit, 7 bit/byte) */
private static final int VARINT_MAX_BYTES = 10;
private ProtoBufType msgType;
private final IntMap values;
/**
* Wire types picked up on the wire or implied by setters (if no other
* type information is available.
*/
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
* description is required if it is necessary to write the protocol buffer for
* data exchange with other systems relying on the .proto file.
*/
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.clear();
wireTypes.clear();
}
/**
* Creates a new instance of the group with the given tag.
*/
public ProtoBuf createGroup(int tag) {
return new ProtoBuf((ProtoBufType) getType().getData(tag));
}
/**
* Appends the given (repeated) tag with the given boolean value.
*/
public void addBool(int tag, boolean value){
insertBool(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given byte[] value.
*/
public void addBytes(int tag, byte[] value){
insertBytes(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given int value.
*/
public void addInt(int tag, int value){
insertInt(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given long value.
*/
public void addLong(int tag, long value){
insertLong(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given float value.
*/
public void addFloat(int tag, float value) {
insertFloat(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given double value.
*/
public void addDouble(int tag, double value) {
insertDouble(tag, getCount(tag), value);
}
/**
* Appends the given (repeated) tag with the given group or message value.
*/
public void addProtoBuf(int tag, ProtoBuf value){
insertProtoBuf(tag, getCount(tag), value);
}
/**
* Adds a new protobuf for the specified tag, setting the child protobuf's
* type correctly for the tag.
* @param tag the tag for which to create a new protobuf
* @return the newly created protobuf
*/
public ProtoBuf addNewProtoBuf(int tag) {
ProtoBuf child = newProtoBufForTag(tag);
addProtoBuf(tag, child);
return child;
}
/**
* Creates and returns a new protobuf for the specified tag, setting the new
* protobuf's type correctly for the tag.
* @param tag the tag for which to create a new protobuf
* @return the newly created protobuf
*/
public ProtoBuf newProtoBufForTag(int tag) {
return new ProtoBuf((ProtoBufType) msgType.getData(tag));
}
/**
* Appends the given (repeated) tag with the given String value.
*/
public void addString(int tag, String value){
insertString(tag, getCount(tag), value);
}
/**
* Returns the boolean value for the given tag.
*/
public boolean getBool(int tag) {
return ((Boolean) getObject(tag, ProtoBufType.TYPE_BOOL))
.booleanValue();
}
/**
* Returns the boolean value for the given repeated tag at the given index.
*/
public boolean getBool(int tag, int index) {
return ((Boolean) getObject(tag, index, ProtoBufType.TYPE_BOOL))
.booleanValue();
}
/**
* Returns the given string tag as byte array.
*/
public byte[] getBytes(int tag) {
return (byte[]) getObject(tag, ProtoBufType.TYPE_DATA);
}
/**
* Returns the given repeated string tag at the given index as byte array.
*/
public byte[] getBytes(int tag, int index) {
return (byte[]) getObject(tag, index, ProtoBufType.TYPE_DATA);
}
/**
* Returns the integer value for the given tag.
*/
public int getInt(int tag) {
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) {
return (int) ((Long) getObject(tag, index,
ProtoBufType.TYPE_INT32)).longValue();
}
/**
* Returns the long value for the given tag.
*/
public long getLong(int tag) {
return ((Long) getObject(tag, ProtoBufType.TYPE_INT64)).longValue();
}
/**
* Returns the long value for the given repeated tag at the given index.
*/
public long getLong(int tag, int index) {
return ((Long) getObject(tag, index, ProtoBufType.TYPE_INT64)).longValue();
}
/**
* Returns the float value for the given tag.
*/
public float getFloat(int tag) {
return Float.intBitsToFloat(getInt(tag));
}
/**
* Returns the float value for the given repeated tag at the given index.
*/
public float getFloat(int tag, int index) {
return Float.intBitsToFloat(getInt(tag, index));
}
/**
* Returns the double value for the given tag.
*/
public double getDouble(int tag) {
return Double.longBitsToDouble(getLong(tag));
}
/**
* Returns the double value for the given repeated tag at the given index.
*/
public double getDouble(int tag, int index) {
return Double.longBitsToDouble(getLong(tag, index));
}
/**
* Returns the group or nested message for the given tag.
*/
public ProtoBuf getProtoBuf(int tag) {
return (ProtoBuf) getObject(tag, ProtoBufType.TYPE_GROUP);
}
/**
* Returns the group or nested message for the given repeated tag at the given
* index.
*/
public ProtoBuf getProtoBuf(int tag, int index) {
return (ProtoBuf) getObject(tag, index, ProtoBufType.TYPE_GROUP);
}
/**
* Returns the string value for a given tag converted to a Java String
* assuming UTF-8 encoding.
*/
public String getString(int tag) {
return (String) getObject(tag, ProtoBufType.TYPE_TEXT);
}
/**
* Returns the string value for a given repeated tag at the given index
* converted to a Java String assuming UTF-8 encoding.
*/
public String getString(int tag, int index) {
return (String) getObject(tag, index, ProtoBufType.TYPE_TEXT);
}
/**
* Returns the type definition of this protocol buffer or group -- if set.
*/
public ProtoBufType getType() {
return msgType;
}
/**
* Sets the type definition of this protocol buffer. Used internally in
* ProtoBufUtil for incremental reading.
*
* @param type the new type
*/
void setType(ProtoBufType type) {
// reject if the type is already set, or value is alreay set.
if (!values.isEmpty() ||
(msgType != null && type != null && type != msgType)) {
throw new IllegalArgumentException();
}
this.msgType = type;
}
/**
* Convenience method for determining whether a tag has a value. Note: in
* contrast to getCount(tag) > 0, this method takes the default value
* into account.
*/
public boolean has(int tag){
return getCount(tag) > 0 || getDefault(tag) != null;
}
/**
* Reads the contents of this ProtocolMessage from the given byte array.
* Currently, this is a shortcut for parse(new ByteArrayInputStream(data)).
* However, this may change in future versions for efficiency reasons.
*
* @param data the byte array the ProtocolMessage is read from
* @throws IOException if an unexpected "End of file" is encountered in
* the byte array
*/
public ProtoBuf parse(byte[] data) throws IOException {
parse(new ByteArrayInputStream(data), data.length);
return this;
}
/**
* Reads the contents of this ProtocolMessage from the given stream.
*
* @param is the input stream providing the contents
* @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
*/
public ProtoBuf parse(InputStream is) throws IOException {
parse(is, Integer.MAX_VALUE);
return this;
}
/**
* Reads the contents of this ProtocolMessage from the given stream, consuming
* at most the given number of bytes.
*
* @param is the input stream providing the contents
* @param available maximum number of bytes to read
* @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, or if we encounter bad data
* while reading.
*/
public int parse(InputStream is, int available) throws IOException {
clear();
while (available > 0) {
long tagAndType = readVarInt(is, true /* permits EOF */);
if (tagAndType == -1){
break;
}
available -= getVarIntSize(tagAndType);
int wireType = ((int) tagAndType) & 0x07;
if (wireType == WIRETYPE_END_GROUP) {
break;
}
int tag = (int) (tagAndType >>> 3);
wireTypes.put(tag, wireType);
// first step: decode tag value
Object value;
switch (wireType) {
case WIRETYPE_VARINT:
long v = readVarInt(is, false);
available -= getVarIntSize(v);
if (isZigZagEncodedType(tag)) {
v = zigZagDecode(v);
}
value = v;
break;
// also used for fixed values
case WIRETYPE_FIXED32:
case WIRETYPE_FIXED64:
v = 0;
int shift = 0;
int count = (wireType == WIRETYPE_FIXED32) ? 4 : 8;
available -= count;
while (count-- > 0) {
long l = is.read();
v |= l << shift;
shift += 8;
}
value = v;
break;
case WIRETYPE_LENGTH_DELIMITED:
int total = (int) readVarInt(is, false);
available -= getVarIntSize(total);
available -= total;
if (getType(tag) == ProtoBufType.TYPE_MESSAGE) {
ProtoBuf msg = new ProtoBuf((ProtoBufType) msgType.getData(tag));
msg.parse(is, total);
value = msg;
} else {
byte[] data = new byte[total];
int pos = 0;
while (pos < total) {
count = is.read(data, pos, total - pos);
if (count <= 0) {
throw new IOException(MSG_EOF);
}
pos += count;
}
value = data;
}
break;
case WIRETYPE_START_GROUP:
ProtoBuf group = new ProtoBuf(msgType == null
? null
: ((ProtoBufType) msgType.getData(tag)));
available = group.parse(is, available);
value = group;
break;
default:
throw new IOException("Unknown wire type " + wireType +
", reading garbage data?");
}
insertObject(tag, getCount(tag), value);
}
if (available < 0){
throw new IOException();
}
return available;
}
/**
* Removes the tag value at the given index.
*/
public void remove(int tag, int index){
int count = getCount(tag);
if (index >= count){
throw new ArrayIndexOutOfBoundsException();
}
if (count == 1){
values.remove(tag);
} else {
Vector v = (Vector) values.get(tag);
v.removeElementAt(index);
}
}
/**
* 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 < 0) {
throw new ArrayIndexOutOfBoundsException(tag);
}
Object o = values.get(tag);
if (o == null){
return 0;
}
return (o instanceof Vector) ? ((Vector) o).size() : 1;
}
/**
* Returns the tag type of the given tag (one of the ProtoBufType.TYPE_XXX
* constants). If no ProtoBufType is set, the wire type is returned. If no
* wire type is available, the wire type is determined by looking at the
* tag value (making sure the wire type is consistent for all values). If
* no value is set, TYPE_UNDEFINED is returned.
*/
public int getType(int tag){
int tagType = ProtoBufType.TYPE_UNDEFINED;
if (msgType != null){
tagType = msgType.getType(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)
? WIRETYPE_VARINT : WIRETYPE_LENGTH_DELIMITED;
}
return tagType;
}
/**
* Returns the number of bytes needed to store this protocol buffer
*/
public int getDataSize() {
return getCachedDataSize(false /* don't trust cache */);
}
/**
* Each Protobuf keeps track of a <code> cachedSize </code> 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.)
*
* <P/>The cache can be cleansed by calling this method with
* trustCache = false.
*
* @param trustCache if the cached size should be trusted. Set false to
* recompuate the size.
*/
private int getCachedDataSize(boolean trustCache) {
if (cachedSize != Integer.MIN_VALUE && trustCache) {
return cachedSize;
}
int size = 0;
IntMap.KeyIterator itr = values.keys();
while(itr.hasNext()) {
int tag = itr.next();
for (int i = 0; i < getCount(tag); i++) {
size += getCachedDataSize(tag, i, trustCache);
}
}
cachedSize = size;
return cachedSize;
}
/**
* Returns the size of the child.
*
* @param tag tag used to determine the type of this child
* @param i used to determine which count this child is
* @param trustSizeCache passed down to #getCachedDataSize()
*/
private int getCachedDataSize(int tag, int i, boolean trustSizeCache) {
int tagSize = getVarIntSize(tag << 3);
switch(getWireType(tag)){
case WIRETYPE_FIXED32:
return tagSize + 4;
case WIRETYPE_FIXED64:
return tagSize + 8;
case WIRETYPE_VARINT:
long value = getLong(tag, i);
if (isZigZagEncodedType(tag)) {
value = zigZagEncode(value);
}
return tagSize + getVarIntSize(value);
case WIRETYPE_START_GROUP:
// take end group into account....
return tagSize + getProtoBuf(tag, i).getDataSize() + tagSize;
}
// take the object as stored
Object o = getObject(tag, i, ProtoBufType.TYPE_UNDEFINED);
int contentSize;
if (o instanceof byte[]) {
contentSize = ((byte[]) o).length;
} else if (o instanceof String) {
contentSize = encodeUtf8((String) o, null, 0);
} else {
contentSize = ((ProtoBuf) o).getCachedDataSize(trustSizeCache);
}
return tagSize + getVarIntSize(contentSize) + contentSize;
}
/**
* Returns the number of bytes needed to encode the given value using
* WIRETYPE_VARINT
*/
private static int getVarIntSize(long i) {
if (i < 0) {
return 10;
}
int size = 1;
while (i >= 128) {
size++;
i >>= 7;
}
return size;
}
/**
* Writes this and nested protocol buffers to the given output stream.
*
* @param os target output stream
* @throws IOException thrown if there is an IOException
*/
public void outputTo(OutputStream os) throws IOException {
// We can't know what changed since we last output, so refresh the children.
getDataSize();
outputToInternal(os);
}
/**
* Recursive output method wrapped by #outputTo()
*
* @param os target output stream
* @throws IOException thrown if there is an IOException
*/
private void outputToInternal(OutputStream os) throws IOException {
IntMap.KeyIterator itr = values.keys();
while (itr.hasNext()) {
int tag = itr.next();
outputField(tag, os);
}
}
/**
* Output a field indicated by the tag to given stream.
*
* @param tag the tag of the field to output.
* @param os target output stream
* @throws IOException thrown if there is an IOException
*/
private void outputField(int tag, OutputStream os) throws IOException {
int size = getCount(tag);
int wireType = getWireType(tag);
int wireTypeTag = (tag << 3) | wireType;
// ignore default values
for (int i = 0; i < size; i++) {
writeVarInt(os, wireTypeTag);
switch (wireType) {
case WIRETYPE_FIXED32:
case WIRETYPE_FIXED64:
long v = ((Long) getObject(tag, i, ProtoBufType.TYPE_INT64))
.longValue();
int cnt = (wireType == WIRETYPE_FIXED32) ? 4 : 8;
for (int b = 0; b < cnt; b++) {
os.write((int) (v & 0x0ff));
v >>= 8;
}
break;
case WIRETYPE_VARINT:
v = ((Long) getObject(tag, i, ProtoBufType.TYPE_INT64)).longValue();
if (isZigZagEncodedType(tag)) {
v = zigZagEncode(v);
}
writeVarInt(os, v);
break;
case WIRETYPE_LENGTH_DELIMITED:
Object o = getObject(tag, i,
getType(tag) == ProtoBufType.TYPE_MESSAGE
? ProtoBufType.TYPE_UNDEFINED
: ProtoBufType.TYPE_DATA);
if (o instanceof byte[]) {
byte[] data = (byte[]) o;
writeVarInt(os, data.length);
os.write(data);
} else {
ProtoBuf msg = (ProtoBuf) o;
writeVarInt(os, msg.getCachedDataSize(true));
msg.outputToInternal(os);
}
break;
case WIRETYPE_START_GROUP:
((ProtoBuf) getObject(tag, i, ProtoBufType.TYPE_GROUP))
.outputToInternal(os);
writeVarInt(os, (tag << 3) | WIRETYPE_END_GROUP);
break;
default:
throw new IllegalArgumentException();
}
}
}
/**
* Returns true if the given tag has a signed type that should be ZigZag-
* encoded on the wire.
*
* ZigZag encoding turns a signed number into
* a non-negative number by mapping negative input numbers to positive odd
* numbers in the output space, and positive input numbers to positive even
* numbers in the output space. This is useful because the wire format
* for protocol buffers requires a large number of bytes to encode
* negative integers, while positive integers take up a smaller number
* of bytes proportional to their magnitude.
*/
private boolean isZigZagEncodedType(int tag) {
int declaredType = getType(tag);
return declaredType == ProtoBufType.TYPE_SINT32 ||
declaredType == ProtoBufType.TYPE_SINT64;
}
/**
* Converts a signed number into a non-negative ZigZag-encoded number.
*/
private static long zigZagEncode(long v) {
v = ((v << 1) ^ -(v >>> 63));
return v;
}
/**
* Converts a non-negative ZigZag-encoded number back into a signed number.
*/
private static long zigZagDecode(long v) {
v = (v >>> 1) ^ -(v & 1);
return v;
}
/**
* Writes this and nested protocol buffers to a byte array.
*
* @throws IOException thrown if there is problem writing the byte array
*/
public byte[] toByteArray() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputTo(baos);
return baos.toByteArray();
}
/**
* Returns the largest tag id used in this message (to simplify testing).
*/
public int maxTag() {
return values.maxKey();
}
/**
* Sets the given tag to the given boolean value.
*/
public void setBool(int tag, boolean value) {
setObject(tag, value ? TRUE : FALSE);
}
/**
* Sets the given tag to the given data bytes.
*/
public void setBytes(int tag, byte[] value) {
setObject(tag, value);
}
/**
* Sets the given tag to the given integer value.
*/
public void setInt(int tag, int value) {
setLong(tag, value);
}
/**
* Sets the given tag to the given long value.
*/
public void setLong(int tag, long value) {
setObject(tag, value);
}
/**
* Sets the given tag to the given double value.
*/
public void setDouble(int tag, double value) {
setLong(tag, Double.doubleToLongBits(value));
}
/**
* Sets the given tag to the given float value.
*/
public void setFloat(int tag, float value) {
setInt(tag, Float.floatToIntBits(value));
}
/**
* Sets the given tag to the given Group or nested Message.
*/
public void setProtoBuf(int tag, ProtoBuf pb) {
setObject(tag, pb);
}
/**
* Sets a new protobuf for the specified tag, setting the child protobuf's
* type correctly for the tag.
* @param tag the tag for which to create a new protobuf
* @return the newly created protobuf
*/
public ProtoBuf setNewProtoBuf(int tag) {
ProtoBuf child = newProtoBufForTag(tag);
setProtoBuf(tag, child);
return child;
}
/**
* Sets the given tag to the given String value.
*/
public void setString(int tag, String value) {
setObject(tag, value);
}
/**
* Inserts the given boolean value for the given tag at the given index.
*/
public void insertBool(int tag, int index, boolean value) {
insertObject(tag, index, value ? TRUE : FALSE);
}
/**
* Inserts the given byte array value for the given tag at the given index.
*/
public void insertBytes(int tag, int index, byte[] value) {
insertObject(tag, index, value);
}
/**
* Inserts the given int value for the given tag at the given index.
*/
public void insertInt(int tag, int index, int value) {
insertLong(tag, index, value);
}
/**
* Inserts the given long value for the given tag at the given index.
*/
public void insertLong(int tag, int index, long value) {
insertObject(tag, index, value);
}
/**
* Inserts the given float value for the given tag at the given index.
*/
public void insertFloat(int tag, int index, float value) {
insertInt(tag, index, Float.floatToIntBits(value));
}
/**
* Inserts the given double value for the given tag at the given index.
*/
public void insertDouble(int tag, int index, double value) {
insertLong(tag, index, Double.doubleToLongBits(value));
}
/**
* Inserts the given group or message for the given tag at the given index.
*/
public void insertProtoBuf(int tag, int index, ProtoBuf pb) {
insertObject(tag, index, pb);
}
/**
* Inserts the given string value for the given tag at the given index.
*/
public void insertString(int tag, int index, String value) {
insertObject(tag, index, value);
}
// ----------------- private stuff below this line ------------------------
private void assertTypeMatch(int tag, Object object){
int tagType = getType(tag);
if (tagType == ProtoBufType.TYPE_UNDEFINED && msgType == null) {
return;
}
if (object instanceof Boolean) {
if (tagType == ProtoBufType.TYPE_BOOL
|| tagType == WIRETYPE_VARINT) {
return;
}
} else if (object instanceof Long) {
switch(tagType){
case WIRETYPE_FIXED32:
case WIRETYPE_FIXED64:
case WIRETYPE_VARINT:
case ProtoBufType.TYPE_BOOL:
case ProtoBufType.TYPE_ENUM:
case ProtoBufType.TYPE_FIXED32:
case ProtoBufType.TYPE_FIXED64:
case ProtoBufType.TYPE_INT32:
case ProtoBufType.TYPE_INT64:
case ProtoBufType.TYPE_SFIXED32:
case ProtoBufType.TYPE_SFIXED64:
case ProtoBufType.TYPE_UINT32:
case ProtoBufType.TYPE_UINT64:
case ProtoBufType.TYPE_SINT32:
case ProtoBufType.TYPE_SINT64:
case ProtoBufType.TYPE_FLOAT:
case ProtoBufType.TYPE_DOUBLE:
return;
}
} else if (object instanceof byte[]){
switch (tagType){
case WIRETYPE_LENGTH_DELIMITED:
case ProtoBufType.TYPE_DATA:
case ProtoBufType.TYPE_MESSAGE:
case ProtoBufType.TYPE_TEXT:
case ProtoBufType.TYPE_BYTES:
case ProtoBufType.TYPE_STRING:
return;
}
} else if (object instanceof ProtoBuf) {
switch (tagType){
case WIRETYPE_LENGTH_DELIMITED:
case WIRETYPE_START_GROUP:
case ProtoBufType.TYPE_DATA:
case ProtoBufType.TYPE_GROUP:
case ProtoBufType.TYPE_MESSAGE:
if (msgType == null || msgType.getData(tag) == null ||
((ProtoBuf) object).msgType == null ||
((ProtoBuf) object).msgType.equals(msgType.getData(tag))) {
return;
}
}
} else if (object instanceof String){
switch (tagType){
case WIRETYPE_LENGTH_DELIMITED:
case ProtoBufType.TYPE_DATA:
case ProtoBufType.TYPE_TEXT:
case ProtoBufType.TYPE_STRING:
return;
}
}
throw new IllegalArgumentException(MSG_MISMATCH + " type:" + msgType +
" tag:" + tag);
}
/**
* Returns the default value for the given tag.
*/
private Object getDefault(int tag){
switch(getType(tag)){
case ProtoBufType.TYPE_UNDEFINED:
case ProtoBufType.TYPE_GROUP:
case ProtoBufType.TYPE_MESSAGE:
return null;
default:
return msgType.getData(tag);
}
}
/**
* Returns the indicated value converted to the given type.
*
* @throws ArrayIndexOutOfBoundsException for invalid tags and indices
* @throws IllegalArgumentException if count is greater than one.
*/
private Object getObject(int tag, int desiredType) {
int count = getCount(tag);
if (count == 0){
return getDefault(tag);
}
if (count > 1){
throw new IllegalArgumentException();
}
return getObject(tag, 0, desiredType);
}
/**
* Returns the indicated value converted to the given type.
*
* @throws ArrayIndexOutOfBoundsException for invalid tags and indices
*/
private Object getObject(int tag, int index, int desiredType) {
if (index >= getCount(tag)) {
throw new ArrayIndexOutOfBoundsException();
}
Object o = values.get(tag);
Vector v = null;
if (o instanceof Vector) {
v = (Vector) o;
o = v.elementAt(index);
}
Object o2 = convert(o, desiredType);
if (o2 != o && o != null) {
if (v == null){
setObject(tag, o2);
} else {
v.setElementAt(o2, index);
}
}
return o2;
}
/**
* Returns the wire type for the given tag. Calls getType() internally,
* so a wire type should be found for all non-empty tags, even if no
* message type is set and the tag was not previously read.
*/
private final int getWireType(int tag) {
int tagType = getType(tag);
switch (tagType) {
case WIRETYPE_VARINT:
case WIRETYPE_FIXED32:
case WIRETYPE_FIXED64:
case WIRETYPE_LENGTH_DELIMITED:
case WIRETYPE_START_GROUP:
case ProtoBufType.TYPE_UNDEFINED:
return tagType;
case ProtoBufType.TYPE_BOOL:
case ProtoBufType.TYPE_INT32:
case ProtoBufType.TYPE_INT64:
case ProtoBufType.TYPE_UINT32:
case ProtoBufType.TYPE_UINT64:
case ProtoBufType.TYPE_SINT32:
case ProtoBufType.TYPE_SINT64:
case ProtoBufType.TYPE_ENUM:
return WIRETYPE_VARINT;
case ProtoBufType.TYPE_DATA:
case ProtoBufType.TYPE_MESSAGE:
case ProtoBufType.TYPE_TEXT:
case ProtoBufType.TYPE_BYTES:
case ProtoBufType.TYPE_STRING:
return WIRETYPE_LENGTH_DELIMITED;
case ProtoBufType.TYPE_DOUBLE:
case ProtoBufType.TYPE_FIXED64:
case ProtoBufType.TYPE_SFIXED64:
return WIRETYPE_FIXED64;
case ProtoBufType.TYPE_FLOAT:
case ProtoBufType.TYPE_FIXED32:
case ProtoBufType.TYPE_SFIXED32:
return WIRETYPE_FIXED32;
case ProtoBufType.TYPE_GROUP:
return WIRETYPE_START_GROUP;
default:
throw new RuntimeException(MSG_UNSUPPORTED + ':' + msgType + '/' +
tag + '/' + tagType);
}
}
/**
* Inserts a value.
*/
private void insertObject(int tag, int index, Object o) {
assertTypeMatch(tag, o);
int count = getCount(tag);
if (count == 0) {
setObject(tag, o);
} else {
Object curr = values.get(tag);
Vector v;
if (curr instanceof Vector) {
v = (Vector) curr;
} else {
v = new Vector();
v.addElement(curr);
values.put(tag, v);
}
v.insertElementAt(o, index);
}
}
/**
* Converts the object if a better suited class exists for the given .proto
* type. If the formats are not compatible, an exception is thrown.
*/
private static Object convert(Object obj, int tagType) {
switch (tagType) {
case ProtoBufType.TYPE_UNDEFINED:
return obj;
case ProtoBufType.TYPE_BOOL:
if (obj instanceof Boolean) {
return obj;
}
switch ((int) ((Long) obj).longValue()) {
case 0:
return FALSE;
case 1:
return TRUE;
default:
throw new IllegalArgumentException(MSG_MISMATCH);
}
case ProtoBufType.TYPE_FIXED32:
case ProtoBufType.TYPE_FIXED64:
case ProtoBufType.TYPE_INT32:
case ProtoBufType.TYPE_INT64:
case ProtoBufType.TYPE_SFIXED32:
case ProtoBufType.TYPE_SFIXED64:
case ProtoBufType.TYPE_SINT32:
case ProtoBufType.TYPE_SINT64:
if (obj instanceof Boolean) {
return ((Boolean) obj).booleanValue() ? 1 : 0;
}
return obj;
case ProtoBufType.TYPE_DATA:
case ProtoBufType.TYPE_BYTES:
if (obj instanceof String) {
return encodeUtf8((String) obj);
} else if (obj instanceof ProtoBuf) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
try {
((ProtoBuf) obj).outputTo(buf);
return buf.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e.toString());
}
}
return obj;
case ProtoBufType.TYPE_TEXT:
case ProtoBufType.TYPE_STRING:
if (obj instanceof byte[]) {
byte[] data = (byte[]) obj;
return decodeUtf8(data, 0, data.length, true);
}
return obj;
case ProtoBufType.TYPE_GROUP:
case ProtoBufType.TYPE_MESSAGE:
if (obj instanceof byte[]) {
try {
return new ProtoBuf(null).parse((byte[]) obj);
} catch (IOException e) {
throw new RuntimeException(e.toString());
}
}
return obj;
default:
// default includes FLOAT and DOUBLE
throw new RuntimeException(MSG_UNSUPPORTED);
}
}
/**
* Reads a variable-size integer (up to 10 bytes for 64 bit) from the
* given input stream.
*
* @param is the stream to read from
* @param permitEOF if true, -1 is returned when EOF is reached instead of
* throwing an IOException
* @return the integer value read from the stream, or -1 if EOF is
* reached and permitEOF is true
* @throws IOException thrown for underlying IO issues and if EOF
* is reached and permitEOF is false
*/
static long readVarInt(InputStream is, boolean permitEOF) throws IOException {
long result = 0;
int shift = 0;
// max 10 byte wire format for 64 bit integer (7 bit data per byte)
for (int i = 0; i < VARINT_MAX_BYTES; i++) {
int in = is.read();
if (in == -1) {
if (i == 0 && permitEOF) {
return -1;
} else {
throw new IOException("EOF");
}
}
result |= ((long) (in & 0x07f)) << shift;
if ((in & 0x80) == 0){
break; // get out early
}
shift += 7;
}
return result;
}
/**
* Internal helper method to set a (single) value. Overwrites all existing
* values.
*/
private void setObject(int tag, Object o) {
if (tag < 0) {
throw new ArrayIndexOutOfBoundsException();
}
if (o != null) {
assertTypeMatch(tag, o);
}
values.put(tag, o);
}
/**
* Write a variable-size integer to the given output stream.
*/
static void writeVarInt(OutputStream os, long value) throws IOException {
for (int i = 0; i < VARINT_MAX_BYTES; i++) {
int toWrite = (int) (value & 0x7f);
value >>>= 7;
if (value == 0) {
os.write(toWrite);
break;
} else {
os.write(toWrite | 0x080);
}
}
}
/**
* Returns a byte array containing the given string, encoded as UTF-8. The
* returned byte array contains at least s.length() bytes and at most
* 4 * s.length() bytes. UTF-16 surrogates are transcoded to UTF-8.
*
* @param s input string to be encoded
* @return UTF-8 encoded input string
*/
static byte[] encodeUtf8(String s) {
int len = encodeUtf8(s, null, 0);
byte[] result = new byte[len];
encodeUtf8(s, result, 0);
return result;
}
/**
* Encodes the given string to UTF-8 in the given buffer or calculates
* the space needed if the buffer is null.
*
* @param s the string to be UTF-8 encoded
* @param buf byte array to write to
* @return new buffer position after writing (which equals the required size
* if pos is 0)
*/
static int encodeUtf8(String s, byte[] buf, int pos){
int len = s.length();
for (int i = 0; i < len; i++){
int code = s.charAt(i);
// surrogate 0xd800 .. 0xdfff?
if (code >= 0x0d800 && code <= 0x0dfff && i + 1 < len){
int codeLo = s.charAt(i + 1);
// 0xfc00 is the surrogate id mask (first six bit of 16 set)
// 0x03ff is the surrogate data mask (remaining 10 bit)
// check if actually a surrogate pair (d800 ^ dc00 == 0400)
if (((codeLo & 0xfc00) ^ (code & 0x0fc00)) == 0x0400){
i += 1;
int codeHi;
if ((codeLo & 0xfc00) == 0x0d800){
codeHi = codeLo;
codeLo = code;
} else {
codeHi = code;
}
code = (((codeHi & 0x3ff) << 10) | (codeLo & 0x3ff)) + 0x10000;
}
}
if (code <= 0x007f) {
if (buf != null){
buf[pos] = (byte) code;
}
pos += 1;
} else if (code <= 0x07FF) {
// non-ASCII <= 0x7FF
if (buf != null){
buf[pos] = (byte) (0xc0 | (code >> 6));
buf[pos + 1] = (byte) (0x80 | (code & 0x3F));
}
pos += 2;
} else if (code <= 0xFFFF){
// 0x7FF < code <= 0xFFFF
if (buf != null){
buf[pos] = (byte) ((0xe0 | (code >> 12)));
buf[pos + 1] = (byte) ((0x80 | ((code >> 6) & 0x3F)));
buf[pos + 2] = (byte) ((0x80 | (code & 0x3F)));
}
pos += 3;
} else {
if (buf != null){
buf[pos] = (byte) ((0xf0 | (code >> 18)));
buf[pos + 1] = (byte) ((0x80 | ((code >> 12) & 0x3F)));
buf[pos + 2] = (byte) ((0x80 | ((code >> 6) & 0x3F)));
buf[pos + 3] = (byte) ((0x80 | (code & 0x3F)));
}
pos += 4;
}
}
return pos;
}
/**
* Decodes an array of UTF-8 bytes to a Java string (UTF-16). The tolerant
* flag determines what to do in case of illegal or unsupported sequences.
*
* @param data input byte array containing UTF-8 data
* @param start decoding start position in byte array
* @param end decoding end position in byte array
* @param tolerant if true, an IllegalArgumentException is thrown for illegal
* UTF-8 codes
* @return the string containing the UTF-8 decoding result
*/
static String decodeUtf8(byte[] data, int start, int end,
boolean tolerant){
StringBuffer sb = new StringBuffer(end - start);
int pos = start;
while (pos < end){
int b = data[pos++] & 0x0ff;
if (b <= 0x7f){
sb.append((char) b);
} else if (b >= 0xf5){ // byte sequence too long
if (!tolerant){
throw new IllegalArgumentException("Invalid UTF8");
}
sb.append((char) b);
} else {
int border = 0xe0;
int count = 1;
int minCode = 128;
int mask = 0x01f;
while (b >= border){
border = (border >> 1) | 0x80;
minCode = minCode << (count == 1 ? 4 : 5);
count++;
mask = mask >> 1;
}
int code = b & mask;
for (int i = 0; i < count; i++){
code = code << 6;
if (pos >= end){
if (!tolerant){
throw new IllegalArgumentException("Invalid UTF8");
}
// otherwise, assume zeroes
} else {
if (!tolerant && (data[pos] & 0xc0) != 0x80){
throw new IllegalArgumentException("Invalid UTF8");
}
code |= (data[pos++] & 0x3f); // six bit
}
}
// illegal code or surrogate code
if (!tolerant && code < minCode || (code >= 0xd800 && code <= 0xdfff)){
throw new IllegalArgumentException("Invalid UTF8");
}
if (code <= 0x0ffff){
sb.append((char) code);
} else { // surrogate UTF16
code -= 0x10000;
sb.append((char) (0xd800 | (code >> 10))); // high 10 bit
sb.append((char) (0xdc00 | (code & 0x3ff))); // low 10 bit
}
}
}
return sb.toString();
}
}