content: /**
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.solr.common.util;
  
  import org.apache.solr.common.SolrDocument;
  import org.apache.solr.common.SolrDocumentList;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.*;
  import java.nio.ByteBuffer;
  
  /**
   * The class is designed to optimaly serialize/deserialize any supported types in Solr response. As we know there are only a limited type of
   * items this class can do it with very minimal amount of payload and code. There are 15 known types and if there is an
   * object in the object tree which does not fall into these types, It must be converted to one of these. Implement an
   * ObjectResolver and pass it over It is expected that this class is used on both end of the pipes. The class has one
   * read method and one write method for each of the datatypes
   * <p/>
   * Note -- Never re-use an instance of this class for more than one marshal or unmarshall operation. Always create a new
   * instance.
   */
  public class JavaBinCodec {
  
    public static final byte
            NULL = 0,
            BOOL_TRUE = 1,
            BOOL_FALSE = 2,
            BYTE = 3,
            SHORT = 4,
            DOUBLE = 5,
            INT = 6,
            LONG = 7,
            FLOAT = 8,
            DATE = 9,
            MAP = 10,
            SOLRDOC = 11,
            SOLRDOCLST = 12,
            BYTEARR = 13,
            ITERATOR = 14,
            /**
             * this is a special tag signals an end. No value is associated with it
             */
            END = 15,
  
            // types that combine tag + length (or other info) in a single byte
            TAG_AND_LEN = (byte) (1 << 5),
            STR = (byte) (1 << 5),
            SINT = (byte) (2 << 5),
            SLONG = (byte) (3 << 5),
            ARR = (byte) (4 << 5), //
            ORDERED_MAP = (byte) (5 << 5), // SimpleOrderedMap (a NamedList subclass, and more common)
            NAMED_LST = (byte) (6 << 5), // NamedList
            EXTERN_STRING = (byte) (7 << 5);
  
  
    private static byte VERSION = 2;
    private ObjectResolver resolver;
    protected FastOutputStream daos;
  
    public JavaBinCodec() {
    }
  
    public JavaBinCodec(ObjectResolver resolver) {
      this.resolver = resolver;
    }
  
    public void marshal(Object nl, OutputStream os) throws IOException {
      daos = FastOutputStream.wrap(os);
      try {
        daos.writeByte(VERSION);
        writeVal(nl);
      } finally {
        daos.flushBuffer();
      }
    }
  
    byte version;
  
    public Object unmarshal(InputStream is) throws IOException {
      FastInputStream dis = FastInputStream.wrap(is);
      version = dis.readByte();
      if (version != VERSION) {
        throw new RuntimeException("Invalid version (expected " + VERSION +
            ", but " + version + ") or the data in not in 'javabin' format");
      }
      return readVal(dis);
    }
  
  
    public SimpleOrderedMap readOrderedMap(FastInputStream dis) throws IOException {
      int sz = readSize(dis);
      SimpleOrderedMap nl = new SimpleOrderedMap();
      for (int i = 0; i < sz; i++) {
        String name = (String) readVal(dis);
        Object val = readVal(dis);
        nl.add(name, val);
      }
      return nl;
    }
  
    public NamedList readNamedList(FastInputStream dis) throws IOException {
      int sz = readSize(dis);
      NamedList nl = new NamedList();
      for (int i = 0; i < sz; i++) {
        String name = (String) readVal(dis);
        Object val = readVal(dis);
        nl.add(name, val);
      }
      return nl;
    }
  
    public void writeNamedList(NamedList nl) throws IOException {
      writeTag(nl instanceof SimpleOrderedMap ? ORDERED_MAP : NAMED_LST, nl.size());
      for (int i = 0; i < nl.size(); i++) {
        String name = nl.getName(i);
        writeExternString(name);
        Object val = nl.getVal(i);
        writeVal(val);
      }
    }
  
    public void writeVal(Object val) throws IOException {
      if (writeKnownType(val)) {
        return;
      } else {
        Object tmpVal = val;
        if (resolver != null) {
          tmpVal = resolver.resolve(val, this);
          if (tmpVal == null) return; // null means the resolver took care of it fully
          if (writeKnownType(tmpVal)) return;
        }
      }
  
      writeVal(val.getClass().getName() + ':' + val.toString());
    }
  
    protected static final Object END_OBJ = new Object();
  
    byte tagByte;
  
    public Object readVal(FastInputStream dis) throws IOException {
      tagByte = dis.readByte();
  
      // if ((tagByte & 0xe0) == 0) {
      // if top 3 bits are clear, this is a normal tag
  
      // OK, try type + size in single byte
      switch (tagByte >>> 5) {
        case STR >>> 5:
          return readStr(dis);
        case SINT >>> 5:
          return readSmallInt(dis);
        case SLONG >>> 5:
          return readSmallLong(dis);
        case ARR >>> 5:
          return readArray(dis);
        case ORDERED_MAP >>> 5:
          return readOrderedMap(dis);
        case NAMED_LST >>> 5:
          return readNamedList(dis);
        case EXTERN_STRING >>> 5:
          return readExternString(dis);
      }
  
      switch (tagByte) {
        case NULL:
          return null;
        case DATE:
          return new Date(dis.readLong());
        case INT:
          return dis.readInt();
        case BOOL_TRUE:
          return Boolean.TRUE;
        case BOOL_FALSE:
          return Boolean.FALSE;
        case FLOAT:
          return dis.readFloat();
        case DOUBLE:
          return dis.readDouble();
        case LONG:
          return dis.readLong();
        case BYTE:
          return dis.readByte();
        case SHORT:
          return dis.readShort();
        case MAP:
          return readMap(dis);
        case SOLRDOC:
          return readSolrDocument(dis);
        case SOLRDOCLST:
          return readSolrDocumentList(dis);
        case BYTEARR:
          return readByteArray(dis);
        case ITERATOR:
          return readIterator(dis);
        case END:
          return END_OBJ;
      }
  
      throw new RuntimeException("Unknown type " + tagByte);
    }
  
    public boolean writeKnownType(Object val) throws IOException {
      if (writePrimitive(val)) return true;
      if (val instanceof NamedList) {
        writeNamedList((NamedList) val);
        return true;
      }
      if (val instanceof SolrDocumentList) { // SolrDocumentList is a List, so must come before List check
        writeSolrDocumentList((SolrDocumentList) val);
        return true;
      }
      if (val instanceof Collection) {
        writeArray((Collection) val);
        return true;
      }
      if (val instanceof Object[]) {
        writeArray((Object[]) val);
        return true;
      }
      if (val instanceof SolrDocument) {
        //this needs special treatment to know which fields are to be written
        if (resolver == null) {
          writeSolrDocument((SolrDocument) val);
        } else {
          Object retVal = resolver.resolve(val, this);
          if (retVal != null) {
            if (retVal instanceof SolrDocument) {
              writeSolrDocument((SolrDocument) retVal);
            } else {
              writeVal(retVal);
            }
          }
        }
        return true;
      }
      if (val instanceof Map) {
        writeMap((Map) val);
        return true;
      }
      if (val instanceof Iterator) {
        writeIterator((Iterator) val);
        return true;
      }
      if (val instanceof Iterable) {
        writeIterator(((Iterable) val).iterator());
        return true;
      }
      return false;
    }
  
    public void writeTag(byte tag) throws IOException {
      daos.writeByte(tag);
    }
  
    public void writeTag(byte tag, int size) throws IOException {
      if ((tag & 0xe0) != 0) {
        if (size < 0x1f) {
          daos.writeByte(tag | size);
        } else {
          daos.writeByte(tag | 0x1f);
          writeVInt(size - 0x1f, daos);
        }
      } else {
        daos.writeByte(tag);
        writeVInt(size, daos);
      }
    }
  
    public void writeByteArray(byte[] arr, int offset, int len) throws IOException {
      writeTag(BYTEARR, len);
      daos.write(arr, offset, len);
    }
  
    public byte[] readByteArray(FastInputStream dis) throws IOException {
      byte[] arr = new byte[readVInt(dis)];
      dis.readFully(arr);
      return arr;
    }
  
    public void writeSolrDocument(SolrDocument doc) throws IOException {
      writeSolrDocument(doc, null);
    }
  
    public void writeSolrDocument(SolrDocument doc, Set<String> fields) throws IOException {
      int count = 0;
      if (fields == null) {
        count = doc.getFieldNames().size();
      } else {
        for (Map.Entry<String, Object> entry : doc) {
          if (fields.contains(entry.getKey())) count++;
        }
      }
      writeTag(SOLRDOC);
      writeTag(ORDERED_MAP, count);
      for (Map.Entry<String, Object> entry : doc) {
        if (fields == null || fields.contains(entry.getKey())) {
          String name = entry.getKey();
          writeExternString(name);
          Object val = entry.getValue();
          writeVal(val);
        }
      }
    }
  
    public SolrDocument readSolrDocument(FastInputStream dis) throws IOException {
      NamedList nl = (NamedList) readVal(dis);
      SolrDocument doc = new SolrDocument();
      for (int i = 0; i < nl.size(); i++) {
        String name = nl.getName(i);
        Object val = nl.getVal(i);
        doc.setField(name, val);
      }
      return doc;
    }
  
    public SolrDocumentList readSolrDocumentList(FastInputStream dis) throws IOException {
      SolrDocumentList solrDocs = new SolrDocumentList();
      List list = (List) readVal(dis);
      solrDocs.setNumFound((Long) list.get(0));
      solrDocs.setStart((Long) list.get(1));
      solrDocs.setMaxScore((Float) list.get(2));
      solrDocs.setSum((Double) list.get(3));
      solrDocs.setMax((Double) list.get(4));
      solrDocs.setMin((Double) list.get(5));
      List l = (List) readVal(dis);
      solrDocs.addAll(l);
      return solrDocs;
    }
  
    public void writeSolrDocumentList(SolrDocumentList docs)
            throws IOException {
      writeTag(SOLRDOCLST);
      List l = new ArrayList(6);
      l.add(docs.getNumFound());
      l.add(docs.getStart());
      l.add(docs.getMaxScore());
      l.add(docs.getSum());
      l.add(docs.getMax());
      l.add(docs.getMin());
      writeArray(l);
      writeArray(docs);
    }
  
    public Map readMap(FastInputStream dis)
            throws IOException {
      int sz = readVInt(dis);
      Map m = new LinkedHashMap();
      for (int i = 0; i < sz; i++) {
        Object key = readVal(dis);
        Object val = readVal(dis);
        m.put(key, val);
  
      }
      return m;
    }
  
    public void writeIterator(Iterator iter) throws IOException {
      writeTag(ITERATOR);
      while (iter.hasNext()) {
        writeVal(iter.next());
      }
      writeVal(END_OBJ);
    }
  
    public List readIterator(FastInputStream fis) throws IOException {
      ArrayList l = new ArrayList();
      while (true) {
        Object o = readVal(fis);
        if (o == END_OBJ) break;
        l.add(o);
      }
      return l;
    }
  
    public void writeArray(List l) throws IOException {
      writeTag(ARR, l.size());
      for (int i = 0; i < l.size(); i++) {
        writeVal(l.get(i));
      }
    }
  
    public void writeArray(Collection coll) throws IOException {
      writeTag(ARR, coll.size());
      for (Object o : coll) {
        writeVal(o);
      }
  
    }
  
    public void writeArray(Object[] arr) throws IOException {
      writeTag(ARR, arr.length);
      for (int i = 0; i < arr.length; i++) {
        Object o = arr[i];
        writeVal(o);
      }
    }
  
    public List readArray(FastInputStream dis) throws IOException {
      int sz = readSize(dis);
      ArrayList l = new ArrayList(sz);
      for (int i = 0; i < sz; i++) {
        l.add(readVal(dis));
      }
      return l;
    }
  
    /**
     * write the string as tag+length, with length being the number of UTF-8 bytes
     */
    public void writeStr(String s) throws IOException {
      if (s == null) {
        writeTag(NULL);
        return;
      }
      int end = s.length();
      int maxSize = end * 4;
      if (bytes == null || bytes.length < maxSize) bytes = new byte[maxSize];
      int upto = 0;
      for(int i=0;i<end;i++) {
        final int code = (int) s.charAt(i);
  
        if (code < 0x80)
          bytes[upto++] = (byte) code;
        else if (code < 0x800) {
          bytes[upto++] = (byte) (0xC0 | (code >> 6));
          bytes[upto++] = (byte)(0x80 | (code & 0x3F));
        } else if (code < 0xD800 || code > 0xDFFF) {
          bytes[upto++] = (byte)(0xE0 | (code >> 12));
          bytes[upto++] = (byte)(0x80 | ((code >> 6) & 0x3F));
          bytes[upto++] = (byte)(0x80 | (code & 0x3F));
        } else {
          // surrogate pair
          // confirm valid high surrogate
          if (code < 0xDC00 && (i < end-1)) {
            int utf32 = (int) s.charAt(i+1);
            // confirm valid low surrogate and write pair
            if (utf32 >= 0xDC00 && utf32 <= 0xDFFF) { 
              utf32 = ((code - 0xD7C0) << 10) + (utf32 & 0x3FF);
              i++;
              bytes[upto++] = (byte)(0xF0 | (utf32 >> 18));
              bytes[upto++] = (byte)(0x80 | ((utf32 >> 12) & 0x3F));
              bytes[upto++] = (byte)(0x80 | ((utf32 >> 6) & 0x3F));
              bytes[upto++] = (byte)(0x80 | (utf32 & 0x3F));
              continue;
            }
          }
          // replace unpaired surrogate or out-of-order low surrogate
          // with substitution character
          bytes[upto++] = (byte) 0xEF;
          bytes[upto++] = (byte) 0xBF;
          bytes[upto++] = (byte) 0xBD;
        }
      }
      writeTag(STR, upto);
      daos.write(bytes, 0, upto);
    }
  
    byte[] bytes;
    char[] chars;
  
    public String readStr(FastInputStream dis) throws IOException {
      int sz = readSize(dis);
      if (chars == null || chars.length < sz) chars = new char[sz];
      if (bytes == null || bytes.length < sz) bytes = new byte[sz];
      dis.readFully(bytes, 0, sz);
      int outUpto=0;
      for (int i = 0; i < sz;) {
        final int b = bytes[i++]&0xff;
        final int ch;
        if (b < 0xc0) {
          assert b < 0x80;
          ch = b;
        } else if (b < 0xe0) {
          ch = ((b&0x1f)<<6) + (bytes[i++]&0x3f);
        } else if (b < 0xf0) {
          ch = ((b&0xf)<<12) + ((bytes[i++]&0x3f)<<6) + (bytes[i++]&0x3f);
        } else {
          assert b < 0xf8;
          ch = ((b&0x7)<<18) + ((bytes[i++]&0x3f)<<12) + ((bytes[i++]&0x3f)<<6) + (bytes[i++]&0x3f);
        }
        if (ch <= 0xFFFF) {
          // target is a character <= 0xFFFF
          chars[outUpto++] = (char) ch;
        } else {
          // target is a character in range 0xFFFF - 0x10FFFF
          final int chHalf = ch - 0x10000;
          chars[outUpto++] = (char) ((chHalf >> 0xA) + 0xD800);
          chars[outUpto++] = (char) ((chHalf & 0x3FF) + 0xDC00);
        }
      }
      return new String(chars, 0, outUpto);
    }
  
    public void writeInt(int val) throws IOException {
      if (val > 0) {
        int b = SINT | (val & 0x0f);
  
        if (val >= 0x0f) {
          b |= 0x10;
          daos.writeByte(b);
          writeVInt(val >>> 4, daos);
        } else {
          daos.writeByte(b);
        }
  
      } else {
        daos.writeByte(INT);
        daos.writeInt(val);
      }
    }
  
    public int readSmallInt(FastInputStream dis) throws IOException {
      int v = tagByte & 0x0F;
      if ((tagByte & 0x10) != 0)
        v = (readVInt(dis) << 4) | v;
      return v;
    }
  
  
    public void writeLong(long val) throws IOException {
      if ((val & 0xff00000000000000L) == 0) {
        int b = SLONG | ((int) val & 0x0f);
        if (val >= 0x0f) {
          b |= 0x10;
          daos.writeByte(b);
          writeVLong(val >>> 4, daos);
        } else {
          daos.writeByte(b);
        }
      } else {
        daos.writeByte(LONG);
        daos.writeLong(val);
      }
    }
  
    public long readSmallLong(FastInputStream dis) throws IOException {
      long v = tagByte & 0x0F;
      if ((tagByte & 0x10) != 0)
        v = (readVLong(dis) << 4) | v;
      return v;
    }
  
    public boolean writePrimitive(Object val) throws IOException {
      if (val == null) {
        daos.writeByte(NULL);
        return true;
      } else if (val instanceof String) {
        writeStr((String) val);
        return true;
      } else if (val instanceof Integer) {
        writeInt(((Integer) val).intValue());
        return true;
      } else if (val instanceof Long) {
        writeLong(((Long) val).longValue());
        return true;
      } else if (val instanceof Float) {
        daos.writeByte(FLOAT);
        daos.writeFloat(((Float) val).floatValue());
        return true;
      } else if (val instanceof Date) {
        daos.writeByte(DATE);
        daos.writeLong(((Date) val).getTime());
        return true;
      } else if (val instanceof Boolean) {
        if ((Boolean) val) daos.writeByte(BOOL_TRUE);
        else daos.writeByte(BOOL_FALSE);
        return true;
      } else if (val instanceof Double) {
        daos.writeByte(DOUBLE);
        daos.writeDouble(((Double) val).doubleValue());
        return true;
      } else if (val instanceof Byte) {
        daos.writeByte(BYTE);
        daos.writeByte(((Byte) val).intValue());
        return true;
      } else if (val instanceof Short) {
        daos.writeByte(SHORT);
        daos.writeShort(((Short) val).intValue());
        return true;
      } else if (val instanceof byte[]) {
        writeByteArray((byte[]) val, 0, ((byte[]) val).length);
        return true;
      }else if (val instanceof ByteBuffer) {
        ByteBuffer buf = (ByteBuffer) val;
        writeByteArray(buf.array(),buf.position(),buf.limit() - buf.position());
        return true;
      } else if (val == END_OBJ) {
        writeTag(END);
        return true;
      }
      return false;
    }
  
  
    public void writeMap(Map val)
            throws IOException {
      writeTag(MAP, val.size());
      for (Map.Entry entry : (Set<Map.Entry>) val.entrySet()) {
        Object key = entry.getKey();
        if (key instanceof String) {
          writeExternString((String) key);
        } else {
          writeVal(key);
        }
        writeVal(entry.getValue());
      }
    }
  
  
    public int readSize(FastInputStream in) throws IOException {
      int sz = tagByte & 0x1f;
      if (sz == 0x1f) sz += readVInt(in);
      return sz;
    }
  
  
    /**
     * Special method for variable length int (copied from lucene). Usually used for writing the length of a
     * collection/array/map In most of the cases the length can be represented in one byte (length < 127) so it saves 3
     * bytes/object
     *
     * @param i
     * @param out
     *
     * @throws IOException
     */
    public static void writeVInt(int i, FastOutputStream out) throws IOException {
      while ((i & ~0x7F) != 0) {
        out.writeByte((byte) ((i & 0x7f) | 0x80));
        i >>>= 7;
      }
      out.writeByte((byte) i);
    }
  
    /**
     * The counterpart for the above
     *
     * @param in
     *
     * @return the int value
     *
     * @throws IOException
     */
    public static int readVInt(FastInputStream in) throws IOException {
      byte b = in.readByte();
      int i = b & 0x7F;
      for (int shift = 7; (b & 0x80) != 0; shift += 7) {
        b = in.readByte();
        i |= (b & 0x7F) << shift;
      }
      return i;
    }
  
  
    public static void writeVLong(long i, FastOutputStream out) throws IOException {
      while ((i & ~0x7F) != 0) {
        out.writeByte((byte) ((i & 0x7f) | 0x80));
        i >>>= 7;
      }
      out.writeByte((byte) i);
    }
  
    public static long readVLong(FastInputStream in) throws IOException {
      byte b = in.readByte();
      long i = b & 0x7F;
      for (int shift = 7; (b & 0x80) != 0; shift += 7) {
        b = in.readByte();
        i |= (long) (b & 0x7F) << shift;
      }
      return i;
    }
  
    private int stringsCount = 0;
    private Map<String, Integer> stringsMap;
    private List<String> stringsList;
  
    public void writeExternString(String s) throws IOException {
      if (s == null) {
        writeTag(NULL);
        return;
      }
      Integer idx = stringsMap == null ? null : stringsMap.get(s);
      if (idx == null) idx = 0;
      writeTag(EXTERN_STRING, idx);
      if (idx == 0) {
        writeStr(s);
        if (stringsMap == null) stringsMap = new HashMap<String, Integer>();
        stringsMap.put(s, ++stringsCount);
      }
  
    }
  
    public String readExternString(FastInputStream fis) throws IOException {
      int idx = readSize(fis);
      if (idx != 0) {// idx != 0 is the index of the extern string
        return stringsList.get(idx - 1);
      } else {// idx == 0 means it has a string value
        String s = (String) readVal(fis);
        if (stringsList == null) stringsList = new ArrayList<String>();
        stringsList.add(s);
        return s;
      }
    }
  
  
    public static interface ObjectResolver {
      public Object resolve(Object o, JavaBinCodec codec) throws IOException;
    }
  
  
  }
  