package de.jpaw.bonaparte.core; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.joda.time.DateTimeZone; import org.joda.time.Instant; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.LocalTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.jpaw.bonaparte.pojos.meta.AlphanumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BasicNumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BinaryElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.EnumDataItem; import de.jpaw.bonaparte.pojos.meta.FieldDefinition; import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.ObjectReference; import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.XEnumDataItem; import de.jpaw.bonaparte.pojos.meta.XEnumDefinition; import de.jpaw.enums.AbstractXEnumBase; import de.jpaw.enums.XEnumFactory; import de.jpaw.util.ByteArray; import de.jpaw.util.CharTestsASCII; import de.jpaw.util.CollectionUtil; public abstract class AbstractCompactParser<E extends Exception> extends Settings implements MessageParser<E>, CompactConstants { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCompactParser.class); protected static final byte [] EMPTY_BYTE_ARRAY = new byte [0]; // most of these are available from DataInput, but need an Exception mapper abstract protected boolean atEnd() throws E; // test for end of input - can be slow (using exceptions for some implementations, therefore only use if no other method exists) abstract protected void pushback(int c); // push back a single token, which must be the last byte read, and can only be consumed by a subsequent needToken() (both flavours) abstract protected E newMPE(int n, String msg); // construct a suitable exception abstract protected BonaPortable createObject(String classname) throws E; // same method - overloading required for possible exception mapping abstract protected BigDecimal checkAndScale(BigDecimal num, NumericElementaryDataItem di) throws E; // exception mapper // basic methods to read data from the stream - all of them will throw an exception if the end of the input has been reached abstract protected int needToken() throws E; // single byte as unsigned (or pushed back character) abstract protected void needToken(int c) throws E; // single byte as unsigned (or pushed back character) abstract protected void skipBytes(int n) throws E; abstract protected long readFixed8ByteLong() throws E; abstract protected long readFixed6ByteLong() throws E; abstract protected int readFixed4ByteInt() throws E; abstract protected int readFixed3ByteInt() throws E; abstract protected int readFixed2ByteInt() throws E; abstract protected char readChar() throws E; abstract protected byte [] readBytes(int n) throws E; // read exactly n bytes and return them in some new byte array abstract protected ByteArray readByteArray(int n) throws E; // read exactly n bytes and return them in some new byte array (avoids array copy) abstract protected String readISO(int len) throws E; // could be coded in general, but provided for performance abstract protected String readUTF16(int len) throws E; // could be coded in general, but provided for performance abstract protected String readUTF8(int len) throws E; // could be coded in general, but provided for performance protected String currentClass; private final boolean useCache = true; private List<BonaPortable> objects; // private int skipDepth = 0; protected AbstractCompactParser() { if (useCache) objects = new ArrayList<BonaPortable>(60); } protected void clearCache() { if (useCache) objects.clear(); } // provide a parser position, if possible. Only used for diagnostic output, return -1 if not available protected int getParseIndex() { return -1; } @Override public void setClassName(String newClassName) { currentClass = newClassName; } @Override public E enumExceptionConverter(IllegalArgumentException e) { return newMPE(MessageParserException.INVALID_ENUM_TOKEN, e.getMessage()); } @Override public E customExceptionConverter(String msg, Exception e) { return newMPE(MessageParserException.CUSTOM_OBJECT_EXCEPTION, e != null ? msg + e.toString() : msg); } private E eNotNumeric(int n, String fieldname) { return newMPE(MessageParserException.NUMBER_PARSING_ERROR, "Numeric token expected but got " + (n & 0xff) + " for field " + currentClass + "." + fieldname); } /** Check for Null. Returns true if null has been encountered and was allowed. Throws an exception in case it was not allowed. Returns false * if no null is next. (Called for field members inside a class.) */ protected boolean checkForNull(FieldDefinition di) throws E { return checkForNull(di.getName(), di.getIsRequired()); } // check for Null called for field members inside a class protected boolean checkForNull(String fieldname, boolean isRequired) throws E { int c = needToken(); if (c == NULL_FIELD) { if (!isRequired) { return true; } else { throw newMPE(MessageParserException.ILLEGAL_EXPLICIT_NULL, fieldname); } } if ((c == PARENT_SEPARATOR) || (c == OBJECT_TERMINATOR)) { if (!isRequired) { // uneat it pushback(c); return true; } else { throw newMPE(MessageParserException.ILLEGAL_IMPLICIT_NULL, fieldname); } } pushback(c); return false; } // check for Null called for field members inside a class protected boolean checkForNullOrNeedToken(String fieldname, boolean isRequired, int token) throws E { int c = needToken(); if (c == token) return false; if (c == NULL_FIELD) { if (!isRequired) { return true; } else { throw newMPE(MessageParserException.ILLEGAL_EXPLICIT_NULL, fieldname); } } if ((c == PARENT_SEPARATOR) || (c == OBJECT_TERMINATOR)) { if (!isRequired) { // uneat it pushback(c); return true; } else { throw newMPE(MessageParserException.ILLEGAL_IMPLICIT_NULL, fieldname); } } throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected 0x%02x, got 0x%02x)", token, c)); } // get the next token, or -1 for explicit null or -2 for implicit null protected int nextToken(String fieldname, boolean isRequired) throws E { int c = needToken(); if (c == NULL_FIELD) { if (!isRequired) { return -1; } else { throw newMPE(MessageParserException.ILLEGAL_EXPLICIT_NULL, fieldname); } } if ((c == PARENT_SEPARATOR) || (c == OBJECT_TERMINATOR)) { if (!isRequired) { // uneat it pushback(c); return -2; } else { throw newMPE(MessageParserException.ILLEGAL_IMPLICIT_NULL, fieldname); } } return c; } // differs to previous implementation: this method does not end if EOF is reached, is does require a subsequent token (for example object end or record end) protected void skipExplicitNulls() throws E { int c; while ((c = needToken()) == NULL_FIELD) { } pushback(c); } @Override public void eatParentSeparator() throws E { eatObjectOrParentSeparator(PARENT_SEPARATOR); } public void eatObjectTerminator() throws E { eatObjectOrParentSeparator(OBJECT_TERMINATOR); } protected void eatObjectOrParentSeparator(int which) throws E { skipExplicitNulls(); // upwards compatibility: skip extra fields if they are blank. int z = needToken(); if (z == which) return; // all good // we have extra data and it is not null. Now the behavior depends on a parser setting ParseSkipNonNulls mySetting = getSkipNonNullsBehavior(); switch (mySetting) { case ERROR: throw newMPE(MessageParserException.EXTRA_FIELDS, String.format("(found byte 0x%02x)", z)); case WARN: LOGGER.warn("{} at index {} parsing class {}", MessageParserException.codeToString(MessageParserException.EXTRA_FIELDS), getParseIndex(), currentClass); // fall through case IGNORE: // the byte encountered next (z) is not what we wanted. Skip non-null fields (or sub objects, even nested) until we find the desired terminator. // skip bytes until we are at end of record (bad!) (thrown by needToken()) or find the terminator pushback(z); // ensure that the byte z is read again! // skipDepth = 0; skipUntilNext(which); } } /** Skips over the data until we find the expected token (usually a record terminator or object terminator or parent separator). * When the method returns, the parser is just behind the expected character. */ protected void skipUntilNext(int which) throws E { int c; // System.out.println(String.format("Descend for skip depth %d for %02x", skipDepth, which)); // ++skipDepth; while ((c = needToken()) != which) { // skip one element, unless the expected one has been found if (c < OBJECT_BEGIN_BASE) continue; // single byte elements int skipBytes = SKIP_BYTES[c - OBJECT_BEGIN_BASE]; if (skipBytes > 0) { // System.out.println(String.format("At pos %04x: ", parseIndex-1) + "skipping " + skipBytes + " bytes for token " + String.format("%02x", c)); // if (c >= 0xb0 && c < 0xc0) // System.out.println(" String is " + new String(inputdata, parseIndex, c - 0xb0 + 1)); skipBytes(skipBytes); } if (skipBytes < 0) { // System.out.println(String.format("At pos %04x: ", parseIndex-1) + "special for token " + String.format("%02x", c)); // special treatment bytes. These are cases where the length is dynamically determined, or which require recursive processing switch (c) { case OBJECT_BEGIN_JSON: // 0xab: new object (by string case OBJECT_BEGIN_BASE: // 0xac: new object (by string case OBJECT_BEGIN_ID: // 0xde: 2 numeric, recurse! case OBJECT_BEGIN_PQON: // 0xdf: object / PQON skipUntilNext(OBJECT_TERMINATOR); break; case COMPRESSED: throw newMPE(MessageParserException.UNSUPPORTED_COMPRESSED, null); // TODO: compressed object not yet supported case COMPACT_BIGINTEGER: case ISO_STRING: case COMPACT_BINARY: case UTF8_STRING: // int len = readInt(needToken(), "(skipping)"); // System.out.println(" String is " + new String(inputdata, parseIndex, len)); // skipBytes(len); skipBytes(readInt(needToken(), "(skipping)")); break; case UTF16_STRING: skipBytes(2 * readInt(needToken(), "(skipping UTF16)")); break; default: throw newMPE(MessageParserException.UNSUPPORTED_TOKEN, null); // TODO (-2 values...) } } } // --skipDepth; // System.out.println(String.format("Return at skip depth %d for %02x", skipDepth, which)); } // upon entry, we know that firstByte is not null (0xa0) protected int readInt(int firstByte, String fieldname) throws E { if (firstByte < 0xa0) { // 1 positive byte numbers if (firstByte <= 31) return firstByte; if (firstByte >= 0x80) return firstByte - 0x60; // 0x20..0x3f throw eNotNumeric(firstByte, fieldname); } if (firstByte <= 0xd0) { if (firstByte <= 0xaa) return 0xa0 - firstByte; // -1 .. -10 if (firstByte < 0xc0) throw eNotNumeric(firstByte, fieldname); // 2 byte number 0...2047 return needToken() + ((firstByte & 0x0f) << 8); } switch (firstByte) { case INT_2BYTE: return readFixed2ByteInt(); case INT_3BYTE: return readFixed3ByteInt(); case INT_4BYTE: return readFixed4ByteInt(); case COMPACT_BOOLEAN_FALSE: return 0; // boolean => int upgrade case COMPACT_BOOLEAN_TRUE: return 1; // boolean => int upgrade default: throw eNotNumeric(firstByte, fieldname); } } protected long readLong(int firstByte, String fieldname) throws E { if (firstByte == INT_6BYTE) return readFixed6ByteLong(); if (firstByte == INT_8BYTE) return readFixed8ByteLong(); return readInt(firstByte, fieldname); } @Override public String readAscii(AlphanumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return readString(di.getName()); } // read a non-null string protected String readString(String fieldname) throws E { int len; int c = needToken(); if (c >= 0x20 && c < 0x80) return String.valueOf((char)c); // single byte string if (c >= EMPTY_FIELD && c <= SHORT_ISO_STRING + 15) { len = c - EMPTY_FIELD; return len > 0 ? readISO(len) : ""; } switch (c) { case UNICODE_CHAR: return String.valueOf(readChar()); // single Unicode char string case ISO_STRING: len = readInt(needToken(), fieldname); return readISO(len); case UTF8_STRING: len = readInt(needToken(), fieldname); return readUTF8(len); case UTF16_STRING: len = readInt(needToken(), fieldname); return readUTF16(len); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected STRING*, got 0x%02x)", c)); } } @Override public String readString(AlphanumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return readString(di.getName()); } @Override public ByteArray readByteArray(BinaryElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); switch (c) { case EMPTY_FIELD: return ByteArray.ZERO_BYTE_ARRAY; // pre 3.6.0 compatibility case COMPACT_BINARY: int len = readInt(needToken(), di.getName()); return readByteArray(len); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected BINARY*, got 0x%02x)", c)); } } @Override public byte[] readRaw(BinaryElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); switch (c) { case EMPTY_FIELD: return EMPTY_BYTE_ARRAY; // pre 3.6.0 compatibility case COMPACT_BINARY: int len = readInt(needToken(), di.getName()); return readBytes(len); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected BINARY*, got 0x%02x)", c)); } } // have the scale already protected BigDecimal readBigdec(int scale, String fieldname) throws E { int c = needToken(); if (c == COMPACT_BIGINTEGER) { // length and mantissa int len = readInt(needToken(), fieldname); return new BigDecimal(new BigInteger(readBytes(len)), scale); } else { return BigDecimal.valueOf(readLong(c, fieldname), scale); } } @Override public BigDecimal readBigDecimal(NumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; String fieldname = di.getName(); int scale = 0; int c = needToken(); if (c == 0) return BigDecimal.ZERO; if (c >= COMPACT_BIGDECIMAL && c <= COMPACT_BIGDECIMAL + 9) { // BigDecimal with scale if (c != COMPACT_BIGDECIMAL) { scale = c - COMPACT_BIGDECIMAL; } else { scale = readInt(needToken(), fieldname); } } else { pushback(c); } // now read mantissa. Either length + digits, or an integer return checkAndScale(readBigdec(scale, fieldname), di); } @Override public Character readCharacter(MiscElementaryDataItem di) throws E { if (checkForNull(di)) return null; return Character.valueOf(readPrimitiveCharacter(di)); } @Override public Boolean readBoolean(MiscElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); if (c == 0 || c == COMPACT_BOOLEAN_FALSE) return Boolean.FALSE; if (c == 1 || c == COMPACT_BOOLEAN_TRUE) return Boolean.TRUE; throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected BOOLEAN 0/1 or false/true, got 0x%02x)", c)); } @Override public Double readDouble(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return Double.valueOf(readPrimitiveDouble(di)); } @Override public Float readFloat(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return Float.valueOf(readPrimitiveFloat(di)); } @Override public Long readLong(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return readLong(needToken(), di.getName()); } @Override public Integer readInteger(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return readInt(needToken(), di.getName()); } @Override public Short readShort(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return (short)readInt(needToken(), di.getName()); } @Override public Byte readByte(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; return (byte)readInt(needToken(), di.getName()); } @Override public BigInteger readBigInteger(BasicNumericElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); if (c == COMPACT_BIGINTEGER) { // length and mantissa int len = readInt(needToken(), di.getName()); return new BigInteger(readBytes(len)); } else { if (c == 0) return BigInteger.ZERO; return BigInteger.valueOf(readLong(c, di.getName())); } } protected LocalDate readDate(String fieldname) throws E { int year = readInt(needToken(), fieldname); int month = readInt(needToken(), fieldname); int day = readInt(needToken(), fieldname); return new LocalDate(year, month, day); } @Override public LocalDate readDay(TemporalElementaryDataItem di) throws E { if (checkForNullOrNeedToken(di.getName(), di.getIsRequired(), COMPACT_DATE)) return null; return readDate(di.getName()); } @Override public LocalTime readTime(TemporalElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); switch (c) { case COMPACT_TIME_MILLIS: return new LocalTime(readInt(needToken(), di.getName()), DateTimeZone.UTC); case COMPACT_TIME: return new LocalTime(readInt(needToken(), di.getName()) * 1000L, DateTimeZone.UTC); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected COMPACT_TIME_*, got 0x%02x)", c)); } } protected LocalDateTime readDateTime(String fieldname, boolean fractional) throws E { int year = readInt(needToken(), fieldname); int month = readInt(needToken(), fieldname); int day = readInt(needToken(), fieldname); int secondsOfDay = readInt(needToken(), fieldname); int millis = 0; if (fractional) { millis = secondsOfDay % 1000; secondsOfDay /= 1000; } return new LocalDateTime(year, month, day, secondsOfDay / 3600, (secondsOfDay % 3600) / 60, secondsOfDay % 60, millis); } @Override public LocalDateTime readDayTime(TemporalElementaryDataItem di) throws E { if (checkForNull(di)) return null; int c = needToken(); switch (c) { case COMPACT_DATETIME: return readDateTime(di.getName(), false); case COMPACT_DATETIME_MILLIS: return readDateTime(di.getName(), true); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected COMPACT_DATETIME_*, got 0x%02x)", c)); } } @Override public Instant readInstant(TemporalElementaryDataItem di) throws E { if (checkForNull(di)) return null; return new Instant(readLong(needToken(), di.getName())); } @Override public <T extends AbstractXEnumBase<T>> T readXEnum(XEnumDataItem di, XEnumFactory<T> factory) throws E { XEnumDefinition spec = di.getBaseXEnum(); if (checkForNull(di.getName(), di.getIsRequired() && !spec.getHasNullToken())) return factory.getNullToken(); String scannedToken = readString(di.getName()); T value = factory.getByToken(scannedToken); if (value == null) { throw newMPE(MessageParserException.INVALID_ENUM_TOKEN, scannedToken); } return value; } protected UUID readUUID() throws E { long msl = readFixed8ByteLong(); long lsl = readFixed8ByteLong(); return new UUID(msl, lsl); } @Override public UUID readUUID(MiscElementaryDataItem di) throws E { if (checkForNullOrNeedToken(di.getName(), di.getIsRequired(), COMPACT_UUID)) return null; return readUUID(); } @Override public double readPrimitiveDouble(BasicNumericElementaryDataItem di) throws E { int c = needToken(); if (c == COMPACT_DOUBLE) { return Double.longBitsToDouble(readFixed8ByteLong()); } // not a float, try upgrade of int to double (doubles of value 0 or 1 are explicitly written as int) return readInt(c, di.getName()); } @Override public float readPrimitiveFloat(BasicNumericElementaryDataItem di) throws E { int c = needToken(); if (c == COMPACT_FLOAT) { return Float.intBitsToFloat(readFixed4ByteInt()); } // not a float, try upgrade of int to float (floats of value 0 or 1 are explicitly written as int) return readInt(c, di.getName()); } @Override public long readPrimitiveLong(BasicNumericElementaryDataItem di) throws E { return readLong(needToken(), di.getName()); } @Override public int readPrimitiveInteger(BasicNumericElementaryDataItem di) throws E { return readInt(needToken(), di.getName()); } @Override public short readPrimitiveShort(BasicNumericElementaryDataItem di) throws E { return (short)readInt(needToken(), di.getName()); } @Override public byte readPrimitiveByte(BasicNumericElementaryDataItem di) throws E { return (byte)readInt(needToken(), di.getName()); } @Override public <R extends BonaPortable> R readObject (ObjectReference di, Class<R> type) throws E { if (checkForNull(di)) { return null; } boolean allowSubtypes = di.getAllowSubclasses(); String fieldname = di.getName(); int c = needToken(); if (useCache && c == OBJECT_AGAIN) { // we reuse an object int objectIndex = readInt(needToken(), fieldname); if (objectIndex >= objects.size()) throw newMPE(MessageParserException.INVALID_BACKREFERENCE, String.format("at %s: requested object %d of only %d available", fieldname, objectIndex, objects.size())); BonaPortable newObject = objects.get(objects.size() - 1 - objectIndex); // 0 is the last one put in, 1 the one before last etc... // check if the object is of expected type if (newObject.getClass() != type) { // check if it is a superclass if (!allowSubtypes || !type.isAssignableFrom(newObject.getClass())) { throw newMPE(MessageParserException.BAD_CLASS, String.format("(got %s, expected %s for %s, subclassing = %b)", newObject.getClass().getSimpleName(), type.getSimpleName(), fieldname, allowSubtypes)); } } return type.cast(newObject); } else if (c == OBJECT_BEGIN_PQON || c == OBJECT_BEGIN_ID || c == OBJECT_BEGIN_BASE) { String previousClass = currentClass; BonaPortable newObject; String classname; if (c == OBJECT_BEGIN_ID) { int factoryId = readInt(needToken(), "$factoryId"); int classId = readInt(needToken(), "$classId"); BonaPortableClass<?> bclass = BonaPortableFactoryById.getByIds(factoryId, classId); if (bclass == null) throw newMPE(MessageParserException.BAD_CLASS_IDS, factoryId + "/" + classId); classname = bclass.getPqon(); newObject = bclass.newInstance(); } else { if (c == OBJECT_BEGIN_BASE) { if (di.getLowerBound() == null) throw newMPE(MessageParserException.INVALID_BASE_CLASS_REFERENCE, ""); classname = di.getLowerBound().getName(); } else { classname = readString(fieldname); if (classname == null || classname.length() == 0) { if (di.getLowerBound() == null) throw newMPE(MessageParserException.INVALID_BASE_CLASS_REFERENCE, ""); // the base class name is referred to, which is contained in the meta data classname = di.getLowerBound().getName(); } needToken(NULL_FIELD); // version not yet allowed } newObject = createObject(classname); } // System.out.println("Creating new obj " + classname + " gave me " + newObject); // check if the object is of expected type if (newObject.getClass() != type) { // check if it is a superclass if (!allowSubtypes || !type.isAssignableFrom(newObject.getClass())) { throw newMPE(MessageParserException.BAD_CLASS, String.format("(got %s, expected %s for %s, subclassing = %b)", newObject.getClass().getSimpleName(), type.getSimpleName(), fieldname, allowSubtypes)); } } // all good here. Parse the contents // if we use the cache, make the object known even before the contents has been parsed, because it may be referenced if the structure is cyclic if (useCache) objects.add(newObject); currentClass = classname; newObject.deserialize(this); eatObjectTerminator(); currentClass = previousClass; return type.cast(newObject); } else { throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected OBJECT_START*, got 0x%02x)", c)); } } @Override public int parseMapStart(FieldDefinition di) throws E { if (checkForNullOrNeedToken(di.getName(), di.getIsAggregateRequired(), MAP_BEGIN)) return COLLECTION_COUNT_NULL; return readInt(needToken(), di.getName()); } @Override public int parseArrayStart(FieldDefinition di, int sizeOfElement) throws E { if (checkForNullOrNeedToken(di.getName(), di.getIsAggregateRequired(), ARRAY_BEGIN)) return COLLECTION_COUNT_NULL; return readInt(needToken(), di.getName()); } @Override public void parseArrayEnd() throws E { } @Override public BonaPortable readRecord() throws E { // there are no record start/end markers in this format return readObject(StaticMeta.OUTER_BONAPORTABLE, BonaPortable.class); } @Override public List<BonaPortable> readTransmission() throws E { List<BonaPortable> results = new ArrayList<BonaPortable>(); while (!atEnd()) results.add(readRecord()); return results; } @Override public boolean readPrimitiveBoolean(MiscElementaryDataItem di) throws E { int c = needToken(); if (c == 0 || c == COMPACT_BOOLEAN_FALSE) return false; if (c == 1 || c == COMPACT_BOOLEAN_TRUE) return true; throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected BOOLEAN 0/1 or false/true, got 0x%02x)", c)); } // default implementations for the next ones... @Override public char readPrimitiveCharacter(MiscElementaryDataItem di) throws E { int c = needToken(); if (c >= 0x20 && c < 0x80) return (char)c; // single byte char (ASCII printable) if (c == SHORT_ISO_STRING) // 1 char ISO string return (char)needToken(); if (c != UNICODE_CHAR) throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected UNICODE_CHAR, got 0x%02x)", c)); return readChar(); } protected void addMapElem(Map<String, Object> map) throws E { // parse field name, then value String key = readString("$jsonObjKey"); // an explicitor implicit null will cause an exception if (!CharTestsASCII.isJavascriptId(key)) throw newMPE(MessageParserException.JSON_ID, key); // now read value Object value = readElementSub(); if (map.put(key, value) != null) throw newMPE(MessageParserException.JSON_DUPLICATE_KEY, key); } // read a non-null map with the start character already parsed protected Map<String, Object> readJsonMapFlexSizeSub() throws E { final Map<String, Object> map = new HashMap<String, Object>(); // now iterate until JSON end is found for (;;) { int c = needToken(); if (c == OBJECT_TERMINATOR) { return map; } pushback(c); addMapElem(map); } } // read a JSON object: Either a flex map or a null is expected here @Override public Map<String, Object> readJson(ObjectReference di) throws E { int c = nextToken(di.getName(), di.getIsRequired()); switch (c) { case -1: case -2: return null; case OBJECT_BEGIN_JSON: return readJsonMapFlexSizeSub(); case MAP_BEGIN: return readMapFixedSizeSub(); default: throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected object start 0xab or 0xfa, got 0x%02x)", c)); } } // read a JSON array: Either an array or a null is expected here @Override public List<Object> readArray(ObjectReference di) throws E { if (checkForNullOrNeedToken(di.getName(), di.getIsRequired(), ARRAY_BEGIN)) return null; // not null, therefore JSON begin return readArraySub(); } protected List<Object> readArraySub() throws E { final int numElem = readInt(needToken(), "$jsonArrayNumElem"); final List<Object> l = new ArrayList<Object>(numElem); for (int i = 0; i < numElem; ++i) l.add(readElementSub()); return l; } protected Map<String, Object> readMapFixedSizeSub() throws E { final int numElem = readInt(needToken(), "$jsonMapNumElem"); final Map<String, Object> map = new HashMap<String, Object>(CollectionUtil.mapInitialSize(numElem)); for (int i = 0; i < numElem; ++i) { addMapElem(map); } return map; } private static final Object [] CONSTANT_OBJECTS = new Object[0xab]; static { int i; for (i = 0; i < 32; ++i) CONSTANT_OBJECTS[i] = Integer.valueOf(i); for (i = 32; i < 128; ++i) CONSTANT_OBJECTS[i] = String.valueOf((char)i); for (i = 0x80; i <= 0x9c; ++i) CONSTANT_OBJECTS[i] = Integer.valueOf(i - 96); // 128 - 32; CONSTANT_OBJECTS[0x9d] = null; // RESERVED CONSTANT_OBJECTS[0x9e] = Boolean.FALSE; CONSTANT_OBJECTS[0x9f] = Boolean.TRUE; CONSTANT_OBJECTS[0xa0] = null; for (i = 0xa1; i <= 0xaa; ++i) CONSTANT_OBJECTS[i] = Integer.valueOf(-(i - 0xa0)); // -1..-10 } // read an optional element protected Object readElementSub() throws E { int c = needToken(); if (c <= 0xaa) { // single byte items return CONSTANT_OBJECTS[c]; } if (c <= 0xcf) { if (c >= 0xb0) { if (c <= 0xbf) { // short single byte string return readISO(c - 0xaf); } else { // 2 byte positive integer: 4 bits + 8 return new Integer(((c & 15) << 8) | needToken()); } } switch (c) { case OBJECT_BEGIN_JSON: //0xab return readJsonMapFlexSizeSub(); case OBJECT_BEGIN_BASE: //0xac // cannot happen within JSON, except if the structure has been redeclared. throw newMPE(MessageParserException.INVALID_BACKREFERENCE, "in JSON element"); case OBJECT_TERMINATOR: //0xad // treat as implicit null: fall through case PARENT_SEPARATOR: //0xae // treat as implicit null pushback(c); return null; case EMPTY_FIELD: //0xaf return EMPTY_STRING; } } // remaining tokens are 0xd0 .. 0xff // hope the compiler converts it to a jump table switch (c) { case COMPACT_FLOAT: //0xd1 return Float.intBitsToFloat(readFixed4ByteInt()); case COMPACT_DOUBLE: //0xd2 return Double.longBitsToDouble(readFixed8ByteLong()); case UNICODE_CHAR: //0xd6 return String.valueOf(readChar()); case COMPACT_UUID: //0xd7 return readUUID(); case COMPACT_DATE: //0xd8 return readDate("$jsonElemDate"); case COMPACT_TIME: //0xd9 return new LocalTime(readInt(needToken(), "$jsonElemTime") * 1000L, DateTimeZone.UTC); case COMPACT_TIME_MILLIS: //0xda return new LocalTime(readInt(needToken(), "$jsonElemTimeMs"), DateTimeZone.UTC); case COMPACT_DATETIME: //0xdb return readDateTime("$jsonElemDateTime", false); case COMPACT_DATETIME_MILLIS: //0xdc return readDateTime("$jsonElemDateTimeMs", true); case OBJECT_AGAIN: //0xdd case OBJECT_BEGIN_ID: //0xde case OBJECT_BEGIN_PQON: //0xdf // object within JSON pushback(c); return readObject(StaticMeta.INNER_BONAPORTABLE, BonaPortable.class); case COMPACT_BIGINTEGER: //0xe0 { int len = readInt(needToken(), "$jsonElemBigintLen"); return new BigInteger(readBytes(len)); } case ISO_STRING: //0xe1 { int len = readInt(needToken(), "$jsonLenISO"); return readISO(len); } case INT_2BYTE: //0xe2 return new Integer(readFixed2ByteInt()); case INT_3BYTE: //0xe3 return new Integer(readFixed3ByteInt()); case INT_4BYTE: //0xe4 return new Integer(readFixed4ByteInt()); case INT_6BYTE: //0xe6 return new Long(readFixed6ByteLong()); case INT_8BYTE: //0xe8 return new Long(readFixed8ByteLong()); case COMPACT_BIGDECIMAL: //0xf0 { int scale = readInt(needToken(), "$jsonBigdecScale"); return readBigdec(scale, "$jsonBigdecMant"); } case 0xf1: case 0xf2: case 0xf3: case 0xf4: case 0xf5: case 0xf6: case 0xf7: case 0xf8: case 0xf9: return readBigdec(c - 0xf0, "$jsonBigdecMant"); case MAP_BEGIN: //0xfa return readMapFixedSizeSub(); case ARRAY_BEGIN: //0xfc return readArraySub(); case UTF16_STRING: //0xfd { int len = readInt(needToken(), "$jsonLenUTF16"); return readUTF16(len); } case COMPACT_BINARY: //0xfe { int len = readInt(needToken(), "$jsonByteArray"); return readByteArray(len); // or readBytes.... / Q: return as [] or ByteArray? } case UTF8_STRING : //0xff { int len = readInt(needToken(), "$jsonLenUTF8"); return readUTF8(len); } default: // compressed, unsupported float types etc... throw newMPE(MessageParserException.UNEXPECTED_CHARACTER, String.format("0x%02x in JSON element", c)); } } // reads a single element @Override public Object readElement(ObjectReference di) throws E { return readElementSub(); } @Override public Integer readEnum(EnumDataItem edi, BasicNumericElementaryDataItem di) throws E { return readInteger(di); } @Override public String readEnum(EnumDataItem edi, AlphanumericElementaryDataItem di) throws E { return readString(di); } }