/* * Copyright 2012 Michael Bischoff * * Licensed 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 de.jpaw.bonaparte.core; import java.io.IOException; import java.io.ObjectInput; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; 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.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.bonaparte.util.BigDecimalTools; import de.jpaw.enums.AbstractXEnumBase; import de.jpaw.enums.XEnumFactory; import de.jpaw.json.JsonException; import de.jpaw.json.JsonParser; import de.jpaw.util.ByteArray; /** * The StringBuilderParser class. * * @author Michael Bischoff * @version $Revision$ * * Implements the deserialization for the internal format using the Externalizable interface. */ public final class ExternalizableParser extends AbstractMessageParser<IOException> implements MessageParser<IOException>, ExternalizableConstants { private static final Logger LOGGER = LoggerFactory.getLogger(ExternalizableParser.class); private final ObjectInput in; private String currentClass = "N/A"; private boolean hasByte = false; private byte pushedBackByte = (byte)0; public ExternalizableParser(ObjectInput in) { this.in = in; } // entry called from generated objects: public static void deserialize(BonaPortable obj, ObjectInput _in) throws IOException, ClassNotFoundException { MessageParser<IOException> _p = new ExternalizableParser(_in); obj.deserialize(_p); } /************************************************************************************************** * Deserialization goes here * @throws IOException **************************************************************************************************/ private byte nextByte() throws IOException { if (hasByte) { hasByte = false; return pushedBackByte; } return in.readByte(); } private byte needToken() throws IOException { return nextByte(); // just a synonym } private void pushBack(byte b) { if (hasByte) { throw new RuntimeException("Duplicate pushback"); } hasByte = true; pushedBackByte = b; } private void needToken(byte c) throws IOException { byte d = nextByte(); if (c != d) { throw new IOException(String.format("Unexpected byte: expected 0x%02x, got 0x%02x in class %s", (int)c, (int)d, currentClass)); } } // check for Null called for field members inside a class private boolean checkForNull(FieldDefinition di) throws IOException { return checkForNull(di.getName(), di.getIsRequired()); } // check for Null called for field members inside a class private boolean checkForNull(String fieldname, boolean isRequired) throws IOException { byte c = nextByte(); if (c == NULL_FIELD) { if (!isRequired) { return true; } else { throw new IOException("Illegal explicit NULL in " + currentClass + "." + fieldname); } } if ((c == PARENT_SEPARATOR) || (c == ARRAY_TERMINATOR) || (c == OBJECT_TERMINATOR)) { if (!isRequired) { // uneat it pushBack(c); return true; } else { throw new IOException("Illegal implicit NULL in " + currentClass + "." + fieldname); } } pushBack(c); return false; } private void skipNulls() throws IOException { for (;;) { byte c = nextByte(); if (c != NULL_FIELD) { pushBack(c); break; } } } @Override public BigDecimal readBigDecimal(NumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } String fieldname = di.getName(); BigDecimal r; byte c = nextByte(); if ((c > FRAC_SCALE_0) && (c <= FRAC_SCALE_18)) { // read fractional part long fraction = readLongNoNull(fieldname); int scale = c-FRAC_SCALE_0; // combine with integral part and create scaled result r = BigDecimal.valueOf((powersOfTen[scale] * readLongNoNull(fieldname)) + fraction, scale); } else { pushBack(c); r = BigDecimal.valueOf(readLongNoNull(fieldname)); } // now check precision, if required, convert! try { return BigDecimalTools.checkAndScale(r, di, -1, currentClass); } catch (MessageParserException a) { throw new IOException("Decimal number does not comply with specs: " + a.getStandardDescription() + " for " + currentClass + "." + fieldname); } } @Override public Character readCharacter(MiscElementaryDataItem di) throws IOException { String tmp = readString(di.getName(), di.getIsRequired(), 1, false, false, true, true); if (tmp == null) { return null; } if (tmp.length() == 0) { throw new IOException("EMPTY CHAR in " + currentClass + "." + di.getName()); } return tmp.charAt(0); } @Override public String readAscii(AlphanumericElementaryDataItem di) throws IOException { return readString(di.getName(), di.getIsRequired(), di.getLength(), di.getDoTrim(), di.getDoTruncate(), di.getAllowControlCharacters(), false); } // readString does the job for Unicode as well as ASCII @Override public String readString(AlphanumericElementaryDataItem di) throws IOException { return readString(di.getName(), di.getIsRequired(), di.getLength(), di.getDoTrim(), di.getDoTruncate(), di.getAllowControlCharacters(), true); } protected String readString(String fieldname, boolean isRequired, int length, boolean doTrim, boolean doTruncate, boolean allowCtrls, boolean allowUnicode) throws IOException { if (checkForNull(fieldname, isRequired)) { return null; } needToken(TEXT); String s = in.readUTF(); if (doTrim) { // skip leading spaces s = s.trim(); } if (doTruncate && (length > 0) && (s.length() > length)) { s = s.substring(0, length); } return s; } @Override public Boolean readBoolean(MiscElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } byte c = nextByte(); if (c == INT_ZERO) { return Boolean.FALSE; } else if (c == INT_ONE) { return Boolean.TRUE; } throw new IOException(String.format("ILLEGAL BOOLEAN: found 0x%02x for %s.%s", (int)c, currentClass, di.getName())); } @Override public ByteArray readByteArray(BinaryElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } needToken(BINARY); assert !hasByte; // readInt() does not respect pushed back byte return ByteArray.read(in); } @Override public byte[] readRaw(BinaryElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } needToken(BINARY); assert !hasByte; // readInt() does not respect pushed back byte byte [] tmp = ByteArray.readBytes(in); int length = di.getLength(); if ((length > 0) && (tmp.length > length)) { throw new IOException(String.format("byte buffer too long (found %d where only %d is allowed in %s.%s)", tmp.length, length, currentClass, di.getName())); } return tmp; } private int readVarInt(String fieldname, int maxBits) throws IOException { byte c = nextByte(); if ((c >= INT_MINUS_ONE) && (c <= NUMERIC_MAX)) { return c - INT_ZERO; } if (c == INT_ONEBYTE) { return in.readByte(); } if ((c == INT_TWOBYTES) && (maxBits >= 16)) { return in.readShort(); } if ((c == INT_FOURBYTES) && (maxBits >= 32)) { return in.readInt(); } throw new IOException(String.format("No suitable integral token: %02x in %s.%s", c, currentClass, fieldname)); } @Override public Byte readByte(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } return (byte)readVarInt(di.getName(), 8); } @Override public Short readShort(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } return (short)readVarInt(di.getName(), 16); } @Override public Integer readInteger(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } return readVarInt(di.getName(), 32); } @Override public Long readLong(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } return readLongNoNull(di.getName()); } private long readLongNoNull(String fieldname) throws IOException { byte c = nextByte(); if (c == INT_EIGHTBYTES) { return in.readLong(); } pushBack(c); return readVarInt(fieldname, 64); } @Override public LocalDateTime readDayTime(TemporalElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } String fieldname = di.getName(); int fractional = 0; byte c = nextByte(); if ((c > FRAC_SCALE_0) && (c <= FRAC_SCALE_18)) { // read fractional part long fraction = readLongNoNull(fieldname); int scale = c-FRAC_SCALE_0; if (scale > 9) { fraction /= powersOfTen[scale - 9]; } else if (scale < 9) { fraction *= powersOfTen[9 - scale]; } fractional = (int)fraction; } else { pushBack(c); } int date = readVarInt(fieldname, 32); if ((date < 0) || (fractional < 0)) { throw new IOException(String.format("negative numbers found for date field: %d.%d in %s.%s", date, fractional, currentClass, fieldname)); } // set the date and time int day, month, year, hour, minute, second; year = date / 10000; month = (date %= 10000) / 100; day = date %= 100; if (di.getHhmmss()) { hour = fractional / 10000000; minute = (fractional %= 10000000) / 100000; second = (fractional %= 100000) / 1000; } else { hour = fractional / 3600000; minute = (fractional %= 3600000) / 60000; second = (fractional %= 60000) / 1000; } fractional %= 1000; // first checks if ((year < 1601) || (year > 2399) || (month == 0) || (month > 12) || (day == 0) || (day > 31)) { throw new IOException(String.format("ILLEGAL DAY: found %d-%d-%d in %s.%s", year, month, day, currentClass, fieldname)); } if ((hour > 23) || (minute > 59) || (second > 59)) { throw new IOException(String.format("ILLEGAL TIME: found %d:%d:%d in %s.%s", hour, minute, second, currentClass, fieldname)); } // now set the return value LocalDateTime result; try { // TODO! default is lenient mode, therefore will not check. Solution // is to read the data again and compare the values of day, month // and year result = new LocalDateTime(year, month, day, hour, minute, second, fractional); } catch (Exception e) { throw new IOException(String.format("exception creating LocalDateTime for %d-%d-%d in %s.%s", year, month, day, currentClass, fieldname)); } return result; } @Override public LocalDate readDay(TemporalElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } String fieldname = di.getName(); int date = readVarInt(fieldname, 32); if (date < 0) { throw new IOException(String.format("negative numbers found for date field: %d in %s.%s", date, currentClass, fieldname)); } // set the date and time int day, month, year; year = date / 10000; month = (date %= 10000) / 100; day = date %= 100; // first checks if ((year < 1601) || (year > 2399) || (month == 0) || (month > 12) || (day == 0) || (day > 31)) { throw new IOException(String.format("ILLEGAL DAY: found %d-%d-%d in %s.%s", year, month, day, currentClass, fieldname)); } // now set the return value LocalDate result; try { // TODO! default is lenient mode, therefore will not check. Solution // is to read the data again and compare the values of day, month // and year result = new LocalDate(year, month, day); } catch (Exception e) { throw new IOException(String.format("exception creating LocalDate for %d-%d-%d in %s.%s", year, month, day, currentClass, fieldname)); } return result; } @Override public LocalTime readTime(TemporalElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } int fractional = 0; byte c = nextByte(); if ((c > FRAC_SCALE_0) && (c <= FRAC_SCALE_18)) { // read fractional part long fraction = readLongNoNull(di.getName()); int scale = c-FRAC_SCALE_0; if (scale > 9) { fraction /= powersOfTen[scale - 9]; } else if (scale < 9) { fraction *= powersOfTen[9 - scale]; } fractional = (int)fraction; } else { pushBack(c); } // set the date and time int hour, minute, second; if (di.getHhmmss()) { hour = fractional / 10000000; minute = (fractional % 10000000) / 100000; second = (fractional % 100000) / 1000; fractional = (fractional % 1000) + 1000 * second + 60000 * minute + 3600000 * hour; } else { hour = fractional / 3600000; minute = (fractional % 3600000) / 60000; second = (fractional % 60000) / 1000; } // first checks if ((hour > 23) || (minute > 59) || (second > 59)) { throw new IOException(String.format("ILLEGAL TIME: found %d:%d:%d in %s.%s", hour, minute, second, currentClass, di.getName())); } return new LocalTime(fractional, DateTimeZone.UTC); } @Override public Instant readInstant(TemporalElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } return new Instant(readLongNoNull(di.getName())); } @Override public int parseMapStart(FieldDefinition di) throws IOException { String fieldname = di.getName(); if (checkForNull(fieldname, false)) { // check it separately in order to give a distinct error message if (di.getIsAggregateRequired()) throw new IOException("ILLEGAL NULL Map in " + currentClass + "." + fieldname); return -1; } needToken(MAP_BEGIN); int foundIndexType = readVarInt(fieldname, 32); if (foundIndexType != di.getMapIndexType().ordinal()) { throw new IOException(String.format("WRONG_MAP_INDEX_TYPE: got %d, expected for %s.%s", foundIndexType, di.getMapIndexType(), currentClass, fieldname)); } int n = readVarInt(fieldname, 32); if ((n < 0) || (n > 1000000000)) { throw new IOException(String.format("ARRAY_SIZE_OUT_OF_BOUNDS: got %d entries (0x%x) for %s.%s", n, n, currentClass, fieldname)); } return n; } @Override public int parseArrayStart(FieldDefinition di, int sizeOfChild) throws IOException { String fieldname = di.getName(); if (checkForNull(fieldname, false)) { // check it separately in order to give a distinct error message if (di.getIsAggregateRequired()) throw new IOException("ILLEGAL NULL List, Set or Array in " + currentClass + "." + fieldname); return -1; } needToken(ARRAY_BEGIN); int n = readVarInt(fieldname, 32); if ((n < 0) || (n > 1000000000)) { throw new IOException(String.format("ARRAY_SIZE_OUT_OF_BOUNDS: got %d entries (0x%x) for %s.%s", n, n, currentClass, fieldname)); } return n; } @Override public void parseArrayEnd() throws IOException { needToken(ARRAY_TERMINATOR); } @Override public BonaPortable readRecord() throws IOException { BonaPortable result; needToken(RECORD_BEGIN); needToken(NULL_FIELD); // version no result = readObject(StaticMeta.OUTER_BONAPORTABLE, BonaPortable.class); needToken(RECORD_TERMINATOR); return result; } @Override public Float readFloat(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } // TODO: accept other numeric types as well & perform conversion needToken(BINARY_FLOAT); return in.readFloat(); } @Override public Double readDouble(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } needToken(BINARY_DOUBLE); return in.readDouble(); } @Override public void eatParentSeparator() throws IOException { eatObjectOrParentSeparator(PARENT_SEPARATOR); } public void eatObjectTerminator() throws IOException { eatObjectOrParentSeparator(OBJECT_TERMINATOR); } protected void eatObjectOrParentSeparator(byte which) throws IOException { skipNulls(); // upwards compatibility: skip extra fields if they are blank. byte 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 new IOException("Extra fields found after object end. Outdated parser? " + currentClass); case WARN: LOGGER.warn("Extra fields found after object end, parsing class {}. Parser outdated?", currentClass); // fall through case IGNORE: // skip bytes until we are at end of record (bad!) (thrown by needToken()) or find the terminator skipUntilNext(which); } } protected void skipUntilNext(byte which) throws IOException { byte c; while ((c = needToken()) != which) { if (c == OBJECT_BEGIN) { // skip nested object! skipUntilNext(OBJECT_TERMINATOR); } } } @Override public BigInteger readBigInteger(BasicNumericElementaryDataItem di) throws IOException { if (checkForNull(di)) { return null; } needToken(BINARY); assert !hasByte; // readInt() does not respect pushed back byte byte [] tmp = ByteArray.readBytes(in); return new BigInteger(tmp); } @Override public <R extends BonaPortable> R readObject (ObjectReference di, Class<R> type) throws IOException { if (checkForNull(di)) { return null; } boolean allowSubtypes = di.getAllowSubclasses(); String fieldname = di.getName(); String previousClass = currentClass; needToken(OBJECT_BEGIN); // version not yet allowed BonaPortable newObject; if (nestedObjectsInternally) { String classname = in.readUTF(); // String revision = readAscii(true, 0, false, false); // long serialUID = in.readLong(); // version not yet allowed needToken(NULL_FIELD); // version not yet allowed try { newObject = BonaPortableFactory.createObject(classname); } catch (MessageParserException e) { throw new IOException(e); } // 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 new IOException(String.format("BAD_CLASS: got %s, expected %s, subclassing = %b in %s.%s", newObject.getClass().getSimpleName(), type.getSimpleName(), allowSubtypes, currentClass, fieldname)); } } // all good here. Parse the contents currentClass = classname; newObject.deserialize(this); eatObjectTerminator(); } else { try { newObject = (BonaPortable)in.readObject(); } catch (ClassNotFoundException e) { throw new IOException(e); } // the following test happens AFTER the class has been read. With the internal variant, it is done BEFORE any fields are parsed. if (newObject.getClass() != type) { // check if it is a superclass if (!allowSubtypes || !type.isAssignableFrom(newObject.getClass())) { throw new IOException(String.format("BAD_CLASS: got %s, expected %s, subclassing = %b in %s.%s", newObject.getClass().getSimpleName(), type.getSimpleName(), allowSubtypes, currentClass, fieldname)); } } } currentClass = previousClass; // pop class name return type.cast(newObject); } @Override public List<BonaPortable> readTransmission() throws IOException { List<BonaPortable> results = new ArrayList<BonaPortable>(); byte c = nextByte(); if (c == TRANSMISSION_BEGIN) { needToken(NULL_FIELD); // version // TODO: parse extensions here while ((c = nextByte()) != TRANSMISSION_TERMINATOR) { // System.out.println("transmission loop: char is " + c); pushBack(c); // push back object def results.add(readRecord()); } // when here, last char was transmission terminator // optionally eat the last one as well? } else if (c == RECORD_BEGIN /* || c == EXTENSION_BEGIN */) { // allow single record as a special case // TODO: parse extensions here pushBack(c); results.add(readRecord()); } else { throw new IOException(String.format("BAD_TRANSMISSION_START: got 0x%02x in %s", (int)c, currentClass)); } // expect that the transmission ends here! TODO: exception if not return results; } @Override public UUID readUUID(MiscElementaryDataItem di) throws IOException { String tmp = readString(di.getName(), di.getIsRequired(), 36, false, false, false, false); if (tmp == null) { return null; } try { return UUID.fromString(tmp); } catch (IllegalArgumentException e) { throw new IOException("UUID in " + currentClass); } } @Override public IOException enumExceptionConverter(IllegalArgumentException e) { return new IOException(e); } @Override public IOException customExceptionConverter(String msg, Exception e) { return new IOException("Cannot construct custom object in " + currentClass + ": " + msg + (e != null ? e.toString() : "")); } @Override public void setClassName(String newClassName) { currentClass = newClassName; } @Override public <T extends AbstractXEnumBase<T>> T readXEnum(XEnumDataItem di, XEnumFactory<T> factory) throws IOException { XEnumDefinition spec = di.getBaseXEnum(); String scannedToken = readString(di.getName(), di.getIsRequired() && !spec.getHasNullToken(), spec.getMaxTokenLength(), true, false, false, true); if (scannedToken == null) return factory.getNullToken(); T value = factory.getByToken(scannedToken); if (value == null) { throw new IOException(String.format("Invalid enum token %s for field %s.%s", scannedToken, currentClass, di.getName())); } return value; } @Override public boolean readPrimitiveBoolean(MiscElementaryDataItem di) throws IOException { int c = needToken(); if (c == INT_ZERO) return false; if (c == INT_ONE) return true; throw new IOException(String.format("Unexpected character: (expected BOOLEAN 0/1, got 0x%02x) in %s.%s", c, currentClass, di.getName())); } // default implementations for the next ones... @Override public char readPrimitiveCharacter(MiscElementaryDataItem di) throws IOException { String tmp = readString(di.getName(), true, 1, false, false, true, true); if (tmp.length() == 0) { throw new IOException("EMPTY CHAR in " + currentClass + "." + di.getName()); } return tmp.charAt(0); } @Override public double readPrimitiveDouble(BasicNumericElementaryDataItem di) throws IOException { needToken(BINARY_DOUBLE); return in.readDouble(); } @Override public float readPrimitiveFloat(BasicNumericElementaryDataItem di) throws IOException { needToken(BINARY_FLOAT); return in.readFloat(); } @Override public long readPrimitiveLong(BasicNumericElementaryDataItem di) throws IOException { return readLongNoNull(di.getName()); } @Override public int readPrimitiveInteger(BasicNumericElementaryDataItem di) throws IOException { return readVarInt(di.getName(), 32); } @Override public short readPrimitiveShort(BasicNumericElementaryDataItem di) throws IOException { return (short)readVarInt(di.getName(), 16); } @Override public byte readPrimitiveByte(BasicNumericElementaryDataItem di) throws IOException { return (byte)readVarInt(di.getName(), 8); } @Override public Map<String, Object> readJson(ObjectReference di) throws IOException { String tmp = readString(di.getName(), di.getIsRequired(), Integer.MAX_VALUE, true, false, true, true); if (tmp == null) return null; try { return new JsonParser(tmp, false).parseObject(); } catch (JsonException e) { throw new IOException(String.format("Invalid JSON for field %s.%s: %s", currentClass, di.getName(), e.getMessage())); } } @Override public List<Object> readArray(ObjectReference di) throws IOException { String tmp = readString(di.getName(), di.getIsRequired(), Integer.MAX_VALUE, true, false, true, true); if (tmp == null) return null; try { return new JsonParser(tmp, false).parseArray(); } catch (JsonException e) { throw new IOException(String.format("Invalid JSON for field %s.%s: %s", currentClass, di.getName(), e.getMessage())); } } @Override public Object readElement(ObjectReference di) throws IOException { String tmp = readString(di.getName(), di.getIsRequired(), Integer.MAX_VALUE, true, false, true, true); if (tmp == null) return null; try { return new JsonParser(tmp, false).parseElement(); } catch (JsonException e) { throw new IOException(String.format("Invalid JSON for field %s.%s: %s", currentClass, di.getName(), e.getMessage())); } } }