aboutsummaryrefslogblamecommitdiff
path: root/src/com/google/common/io/protocol/ProtoBuf.java
blob: ae7e4a6d7e80adc3ba0e7ca7893f68c52ea9c46c (plain) (tree)
1
2
3
4
5
6
7
8
9








                                                 
                                  






















                                                                                










                                                              


                                                                   









                                                                            
                               
                              




                                                                        





                                                                                  







                                                                                







                                                              

   
     


                                             

                      



























































































































                                                                              
     





























































































                                                                                

                                                                 




















































                                                                                

                                                                         















                                                                      
                                   








                                         
                    















                                                             
                    

































                                                                             

                                                                 



















                                                 
                         
            
                                          







                                                                               


                                                           

                                

                                                    
     
                               


















                                                                             




                                                        
     
 

                                                                      

                                                             

                                                      
 

                   
 



                                                                      




















                                                                         
                 


                                           
                                               
                                                      
       



                      
   






                                                              
     
                                                                         
















                                                                     
 

                                                              
 
                    

                              



                                                    
                                                                     
     
 


















                                                                        
     


                                                                        
                                                          

                                                            



                                                                                
 










































































                                                                              















































                                                                             
 



                                                                            
                           


























                                                    
                          


































































                                                                              
                                    






































































                                                                              
                                      









                                                                

                                                                         






























































                                                                           
                               















































































                                                                           
                                    





                                   
                           








                                                                              
                                                          
























                                                             
                                                        



















































































                                                                                

                                                 



                              
                       
















































































































































































                                                                               
// 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) &gt; 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();
  }

}