/* * 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.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.UUID; 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.enums.AbstractXEnumBase; import de.jpaw.enums.XEnumFactory; import de.jpaw.util.Base64; import de.jpaw.util.ByteArray; import de.jpaw.util.CharTestsASCII; // according to http://stackoverflow.com/questions/469695/decode-base64-data-in-java , xml.bind is included in Java 6 SE //import javax.xml.bind.DatatypeConverter; /** * The StringBuilderParser class. * * @author Michael Bischoff * @version $Revision$ * * Implements the deserialization for the bonaparte format using StringBuilder. */ public final class StringBuilderParser extends AbstractPartialJsonStringParser implements MessageParser<MessageParserException>, StringBuilderConstants { private static final Logger LOGGER = LoggerFactory.getLogger(StringBuilderParser.class); private CharSequence work; // for parser private int parseIndex; // for parser private int messageLength; // for parser private String currentClass; private final boolean useCache = true; private List<BonaPortable> objects; protected final StringParserUtil stringParser = new StringParserUtil(new ParsePositionProvider() { @Override public int getParsePosition() { return parseIndex; } @Override public String getCurrentClassName() { return currentClass; } }); @Override protected MessageParserException newMPE(int errorCode, FieldDefinition di, String msg) { return new MessageParserException(errorCode, di.getName(), parseIndex, currentClass, msg); } /** Quick conversion utility method, for use by code generators. (null safe) */ public static <T extends BonaPortable> T unmarshal(String x, ObjectReference di, Class<T> expectedClass) throws MessageParserException { if (x == null || x.length() == 0) return null; return new StringBuilderParser(x, 0, -1).readObject(di, expectedClass); } /** Assigns a new source to subsequent parsing operations. */ public final void setSource(CharSequence src, int offset, int length) { work = src; parseIndex = offset; messageLength = length; if (useCache) objects.clear(); } /** Assigns a new source to subsequent parsing operations. */ public final void setSource(CharSequence src) { work = src; parseIndex = 0; messageLength = src.length(); if (useCache) objects.clear(); } /** Create a processor for parsing a buffer. */ public StringBuilderParser(CharSequence work, int offset, int length) { if (useCache) objects = new ArrayList<BonaPortable>(60); setSource(work, offset, length < 0 ? work.length() : length); // -1 means full array size currentClass = "N/A"; } /************************************************************************************************** * Deserialization goes here **************************************************************************************************/ private char needToken() throws MessageParserException { if (parseIndex >= messageLength) { throw new MessageParserException(MessageParserException.PREMATURE_END, null, parseIndex, currentClass); } return work.charAt(parseIndex++); } private void needToken(char c) throws MessageParserException { if (parseIndex >= messageLength) { throw new MessageParserException(MessageParserException.PREMATURE_END, String.format("(expected 0x%02x)", (int)c), parseIndex, currentClass); } char d = work.charAt(parseIndex++); if (c != d) { throw new MessageParserException(MessageParserException.UNEXPECTED_CHARACTER, String.format("(expected 0x%02x, got 0x%02x)", (int)c, (int)d), parseIndex, currentClass); } } /* If byte c occurs, eat it */ private void skipChar(char c) { if ((parseIndex < messageLength) && (work.charAt(parseIndex) == c)) { ++parseIndex; } } // check for Null called for field members inside a class private boolean checkForNull(FieldDefinition di) throws MessageParserException { return checkForNull(di.getName(), di.getIsRequired()); } protected int readInteger(String fieldname) throws MessageParserException { checkForNull(fieldname, true); return Integer.parseInt(nextIndexParseAscii(fieldname, false, false, false)); } // check for Null called for field members inside a class private boolean checkForNull(String fieldname, boolean isRequired) throws MessageParserException { char c = needToken(); if (c == NULL_FIELD) { if (!isRequired) { return true; } else { throw new MessageParserException(MessageParserException.ILLEGAL_EXPLICIT_NULL, fieldname, parseIndex, currentClass); } } if ((c == PARENT_SEPARATOR) || (c == ARRAY_TERMINATOR) || (c == OBJECT_TERMINATOR)) { if (!isRequired) { // uneat it --parseIndex; return true; } else { throw new MessageParserException(MessageParserException.ILLEGAL_IMPLICIT_NULL, fieldname, parseIndex, currentClass); } } --parseIndex; return false; } private void skipLeadingSpaces() { while (parseIndex < messageLength) { char c = work.charAt(parseIndex); if ((c != ' ') && (c != '\t')) { break; } // skip leading blanks ++parseIndex; } } private void skipNulls() { while (parseIndex < messageLength) { char c = work.charAt(parseIndex); if (c != NULL_FIELD) { break; } // skip trailing NULL objects ++parseIndex; } } private String nextIndexParseAscii(String fieldname, boolean allowSign, boolean allowDecimalPoint, boolean allowExponent) throws MessageParserException { final int BUFFER_SIZE = 40; boolean allowSignNextIteration = false; boolean gotAnyDigit = false; StringBuffer tmp = new StringBuffer(BUFFER_SIZE); // skipBlanks: does not hurt! skipLeadingSpaces(); if ((parseIndex < messageLength) && (work.charAt(parseIndex) == '+')) { // allow positive sign in any case (but not followed by a minus) ++parseIndex; allowSign = false; } while (parseIndex < messageLength) { char c = work.charAt(parseIndex); if (c == FIELD_TERMINATOR) { if (!gotAnyDigit) { throw new MessageParserException(MessageParserException.NO_DIGITS_FOUND, fieldname, parseIndex, currentClass); } ++parseIndex; // eat it! return tmp.toString(); } if (c == '-') { if (!allowSign) { throw new MessageParserException(MessageParserException.SUPERFLUOUS_SIGN, fieldname, parseIndex, currentClass); } } else if (c == '.') { if (!allowDecimalPoint) { throw new MessageParserException(MessageParserException.SUPERFLUOUS_DECIMAL_POINT, fieldname, parseIndex, currentClass); } allowDecimalPoint = false; // no 2 in a row allowed } else if ((c == 'e') || (c == 'E')) { if (!allowExponent) { throw new MessageParserException(MessageParserException.SUPERFLUOUS_EXPONENT, fieldname, parseIndex, currentClass); } if (!gotAnyDigit) { throw new MessageParserException(MessageParserException.NO_DIGITS_FOUND, fieldname, parseIndex, currentClass); } allowSignNextIteration = true; allowExponent = false; allowDecimalPoint = false; } else if (CharTestsASCII.isAsciiDigit(c)) { gotAnyDigit = true; } else { throw new MessageParserException(MessageParserException.ILLEGAL_CHAR_NOT_NUMERIC, fieldname, parseIndex, currentClass); } if (tmp.length() >= BUFFER_SIZE) { throw new MessageParserException(MessageParserException.NUMERIC_TOO_LONG, fieldname, parseIndex, currentClass); } tmp.append(c); ++parseIndex; allowSign = allowSignNextIteration; allowSignNextIteration = false; } // end of message without appropriate terminator character throw new MessageParserException(MessageParserException.MISSING_TERMINATOR, fieldname, parseIndex, currentClass); } @Override public Character readCharacter(MiscElementaryDataItem di) throws MessageParserException { return stringParser.readCharacter(di, readString(di.getName(), di.getIsRequired(), 1, false, false, true, true)); } @Override public String readAscii(AlphanumericElementaryDataItem di) throws MessageParserException { 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 MessageParserException { 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 MessageParserException { if (checkForNull(fieldname, isRequired)) { return null; } // OK, read it. The provided length can be huge, use a sensible starting size if it is too big StringBuffer tmp = new StringBuffer(length == 0 || length > 256 ? 256 : length); char c; if (doTrim) { // skip leading spaces skipLeadingSpaces(); } while ((c = needToken()) != FIELD_TERMINATOR) { if (allowUnicode) { // checks for Unicode characters if (c < ' ') { if (allowCtrls && (c == '\t')) { // special case: unescaped TAB character allowed } else if (allowCtrls && (c == ESCAPE_CHAR)) { c = needToken(); if ((c < 0x40) || (c >= 0x60)) { throw new MessageParserException(MessageParserException.ILLEGAL_ESCAPE_SEQUENCE, String.format("(found 0x%02x for %s)", (int)c, fieldname), parseIndex, currentClass); } c -= 0x40; } else { throw new MessageParserException(MessageParserException.ILLEGAL_CHAR_CTRL, fieldname, parseIndex, currentClass); } } } else { if (!CharTestsASCII.isAsciiPrintable(c)) { throw new MessageParserException(MessageParserException.ILLEGAL_CHAR_ASCII, String.format("(found 0x%02x for %s)", (int)c, fieldname), parseIndex, currentClass); } } tmp.append(c); } if (doTrim) { int l = tmp.length(); // trim trailing blanks while (l > 0) { char d = tmp.charAt(l-1); if ((d != ' ') && (d != '\t')) { break; // l is correct length } --l; } if (l < tmp.length()) { tmp.setLength(l); } } if (length > 0) { // have limits on max size if (tmp.length() > length) { if (doTruncate) { tmp.setLength(length); } else { throw new MessageParserException(MessageParserException.STRING_TOO_LONG, String.format("(exceeds length %d for %s, got so far %s)", length, fieldname, tmp.toString()), parseIndex, currentClass); } } } return tmp.toString(); } @Override public Boolean readBoolean(MiscElementaryDataItem di) throws MessageParserException { boolean result; if (checkForNull(di)) { return null; } char c = needToken(); if (c == '0') { result = false; } else if (c == '1') { result = true; } else { throw new MessageParserException(MessageParserException.ILLEGAL_BOOLEAN, String.format("(found 0x%02x for %s)", (int)c, di.getName()), parseIndex, currentClass); } needToken(FIELD_TERMINATOR); return Boolean.valueOf(result); } @Override public ByteArray readByteArray(BinaryElementaryDataItem di) throws MessageParserException { byte [] tmp = readRaw(di); if (tmp == null) { return null; } return new ByteArray(tmp); // TODO: this call does an unnecessary copy } @Override public byte[] readRaw(BinaryElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } int i = parseIndex; // find next occurence of field terminator while ((i < messageLength) && (work.charAt(i) != FIELD_TERMINATOR)) { ++i; } if (i == messageLength) { throw new MessageParserException(MessageParserException.MISSING_TERMINATOR, di.getName(), parseIndex, currentClass); } String tmp = work.subSequence(parseIndex, i).toString(); // TODO: too many temporary objects created. This could be improved. parseIndex = i+1; try { byte [] btmp = tmp.getBytes(); return Base64.decode(btmp, 0, btmp.length); } catch (IllegalArgumentException e) { throw new MessageParserException(MessageParserException.BASE64_PARSING_ERROR, di.getName(), parseIndex, currentClass); } // return DatatypeConverter.parseHexBinary(tmp); } @Override public LocalDateTime readDayTime(TemporalElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readDayTime(di, nextIndexParseAscii(di.getName(), false, di.getFractionalSeconds() >= 0, false)); } @Override public LocalDate readDay(TemporalElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readDay(di, nextIndexParseAscii(di.getName(), false, false, false)); // parse an unsigned numeric string without exponent } @Override public LocalTime readTime(TemporalElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readTime(di, nextIndexParseAscii(di.getName(), false, di.getFractionalSeconds() > 0, false)); // parse an unsigned numeric string without exponent } @Override public Instant readInstant(TemporalElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readInstant(di, nextIndexParseAscii(di.getName(), false, true, false)); // parse an unsigned numeric string without exponent } @Override public int parseMapStart(FieldDefinition di) throws MessageParserException { String fieldname = di.getName(); if (checkForNull(fieldname, false)) { // check it separately in order to give a distinct error message if (di.getIsAggregateRequired()) throw new MessageParserException(MessageParserException.NULL_MAP_NOT_ALLOWED_HERE, fieldname, parseIndex, currentClass); return -1; } needToken(MAP_BEGIN); int foundIndexType = readInteger(fieldname); if (foundIndexType != di.getMapIndexType().ordinal()) { throw new MessageParserException(MessageParserException.WRONG_MAP_INDEX_TYPE, String.format("(got %d, expected for %s)", foundIndexType, di.getMapIndexType(), fieldname), parseIndex, currentClass); } int n = readInteger(fieldname); if ((n < 0) || (n > 1000000000)) { throw new MessageParserException(MessageParserException.ARRAY_SIZE_OUT_OF_BOUNDS, String.format("(got %d entries (0x%x) for %s)", n, n, fieldname), parseIndex, currentClass); } return n; } @Override public int parseArrayStart(FieldDefinition di, int sizeOfChild) throws MessageParserException { String fieldname = di.getName(); if (checkForNull(fieldname, false)) { if (di.getIsAggregateRequired()) throw new MessageParserException(MessageParserException.NULL_COLLECTION_NOT_ALLOWED, fieldname, parseIndex, currentClass); return -1; } needToken(ARRAY_BEGIN); int n = readInteger(fieldname); if ((n < 0) || (n > 1000000000)) { throw new MessageParserException(MessageParserException.ARRAY_SIZE_OUT_OF_BOUNDS, String.format("(got %d entries (0x%x) for %s)", n, n, fieldname), parseIndex, currentClass); } return n; } @Override public void parseArrayEnd() throws MessageParserException { needToken(ARRAY_TERMINATOR); } protected void skipOptionalBom() throws MessageParserException { if (needToken() != BOM) { --parseIndex; // uneat it } // else: skip it and expect RECORD_BEGIN } @Override public BonaPortable readRecord() throws MessageParserException { BonaPortable result; skipOptionalBom(); needToken(RECORD_BEGIN); needToken(NULL_FIELD); // version no result = readObject(StaticMeta.OUTER_BONAPORTABLE, BonaPortable.class); skipChar(RECORD_OPT_TERMINATOR); needToken(RECORD_TERMINATOR); return result; } @Override public Byte readByte(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readByte(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), false, false)); } @Override public Short readShort(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readShort(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), false, false)); } @Override public Integer readInteger(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readInteger(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), false, false)); } @Override public Long readLong(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readLong(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), false, false)); } @Override public Float readFloat(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readFloat(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), true, true)); } @Override public Double readDouble(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readDouble(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), true, true)); } @Override public BigInteger readBigInteger(BasicNumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readBigInteger(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), false, false)); } @Override public BigDecimal readBigDecimal(NumericElementaryDataItem di) throws MessageParserException { if (checkForNull(di)) { return null; } return stringParser.readBigDecimal(di, nextIndexParseAscii(di.getName(), di.getIsSigned(), true, false)); } @Override public void eatParentSeparator() throws MessageParserException { eatObjectOrParentSeparator(PARENT_SEPARATOR); } public void eatObjectTerminator() throws MessageParserException { eatObjectOrParentSeparator(OBJECT_TERMINATOR); } protected void eatObjectOrParentSeparator(char which) throws MessageParserException { skipNulls(); // upwards compatibility: skip extra fields if they are blank. char 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 MessageParserException(MessageParserException.EXTRA_FIELDS, String.format("(found byte 0x%02x)", z), parseIndex, currentClass); case WARN: LOGGER.warn("{} at index {} parsing class {}", MessageParserException.codeToString(MessageParserException.EXTRA_FIELDS), parseIndex, 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 --parseIndex; // ensure that the byte z is read again! 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(char which) throws MessageParserException { char c; while ((c = needToken()) != which) { if (c == OBJECT_BEGIN) { // skip nested object! skipUntilNext(OBJECT_TERMINATOR); } } } @Override public <R extends BonaPortable> R readObject (ObjectReference di, Class<R> type) throws MessageParserException { if (checkForNull(di)) { return null; } boolean allowSubtypes = di.getAllowSubclasses(); String fieldname = di.getName(); if (useCache && parseIndex < messageLength && work.charAt(parseIndex) == OBJECT_AGAIN) { // we reuse an object ++parseIndex; int objectIndex = readInteger(fieldname); if (objectIndex >= objects.size()) throw new MessageParserException(MessageParserException.INVALID_BACKREFERENCE, String.format( "at %s: requested object %d of only %d available", fieldname, objectIndex, objects.size()), parseIndex, currentClass); 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 new MessageParserException(MessageParserException.BAD_CLASS, String.format("(got %s, expected %s for %s, subclassing = %b)", newObject.getClass().getSimpleName(), type.getSimpleName(), fieldname, allowSubtypes), parseIndex, currentClass); } } return type.cast(newObject); } else { needToken(OBJECT_BEGIN); // version not yet allowed String previousClass = currentClass; String classname = readString(fieldname, true, 0, true, false, false, false); // String revision = readAscii(true, 0, false, false); needToken(NULL_FIELD); // version not yet allowed BonaPortable newObject = BonaPortableFactory.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 new MessageParserException(MessageParserException.BAD_CLASS, String.format("(got %s, expected %s for %s, subclassing = %b)", newObject.getClass().getSimpleName(), type.getSimpleName(), fieldname, allowSubtypes), parseIndex, currentClass); } } // 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); } } @Override public List<BonaPortable> readTransmission() throws MessageParserException { List<BonaPortable> results = new ArrayList<BonaPortable>(); char c = needToken(); if (c == TRANSMISSION_BEGIN) { needToken(NULL_FIELD); // version // TODO: parse extensions here while ((c = needToken()) != TRANSMISSION_TERMINATOR) { // System.out.println("transmission loop: char is " + c); --parseIndex; // 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 --parseIndex; results.add(readRecord()); } else { throw new MessageParserException(MessageParserException.BAD_TRANSMISSION_START, String.format("(got 0x%02x)", (int)c), parseIndex, currentClass); } // expect that the transmission ends here! TODO: exception if not return results; } @Override public UUID readUUID(MiscElementaryDataItem di) throws MessageParserException { return stringParser.readUUID(di, readString(di.getName(), di.getIsRequired(), 36, false, false, false, false)); } @Override public MessageParserException enumExceptionConverter(IllegalArgumentException e) { return new MessageParserException(MessageParserException.INVALID_ENUM_TOKEN, e.getMessage(), parseIndex, currentClass); } @Override public MessageParserException customExceptionConverter(String msg, Exception e) { return new MessageParserException(MessageParserException.CUSTOM_OBJECT_EXCEPTION, e != null ? msg + e.toString() : msg, parseIndex, currentClass); } @Override public void setClassName(String newClassName) { currentClass = newClassName; } @Override public <T extends AbstractXEnumBase<T>> T readXEnum(XEnumDataItem di, XEnumFactory<T> factory) throws MessageParserException { XEnumDefinition spec = di.getBaseXEnum(); String scannedToken = readString(di.getName(), di.getIsRequired() && !spec.getHasNullToken(), spec.getMaxTokenLength(), true, false, false, true); return stringParser.readXEnum(di, factory, scannedToken); } @Override public boolean readPrimitiveBoolean(MiscElementaryDataItem di) throws MessageParserException { boolean result; char c = needToken(); if (c == '0') { result = false; } else if (c == '1') { result = true; } else { throw new MessageParserException(MessageParserException.ILLEGAL_BOOLEAN, String.format("(found 0x%02x for %s)", (int)c, di.getName()), parseIndex, currentClass); } needToken(FIELD_TERMINATOR); return result; } @Override protected String getString(FieldDefinition di) throws MessageParserException { return readString(di.getName(), di.getIsRequired(), Integer.MAX_VALUE, true, false, true, true); } @Override public Object readElement(ObjectReference di) throws MessageParserException { // hack to allow for BonaPortable here if (parseIndex < messageLength) { char c = work.charAt(parseIndex); if (c == OBJECT_AGAIN || c == OBJECT_BEGIN) return readObject(di, BonaPortable.class); } return super.readElement(di); } }