/*
* 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.nio.charset.Charset;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
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.enums.BonaNonTokenizableEnum;
import de.jpaw.bonaparte.enums.BonaTokenizableEnum;
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.util.FixASCII;
import de.jpaw.enums.XEnum;
import de.jpaw.util.Base64;
import de.jpaw.util.ByteArray;
import de.jpaw.util.ByteBuilder;
/**
* Implements the serialization for the bonaparte format into byte arrays, using the {@link de.jpaw.util.ByteBuilder ByteBuilder} class, which is similar to the well known {@link java.lang.StringBuilder StringBuilder}.
*
* @author Michael Bischoff
*
*/
public class ByteArrayComposer extends AbstractMessageComposer<RuntimeException> implements BufferedMessageComposer<RuntimeException>, ByteArrayConstants {
private static final Logger LOGGER = LoggerFactory.getLogger(ByteArrayComposer.class);
private final boolean useCache;
private final Map<BonaCustom,Integer> objectCache;
private int numberOfObjectsSerialized;
private int numberOfObjectReuses;
// variables for serialization
private ByteBuilder work;
/** Quick conversion utility method, for use by code generators. (null safe) */
public static byte [] marshal(ObjectReference di, BonaPortable x) {
if (x == null)
return null;
ByteArrayComposer bac = new ByteArrayComposer();
bac.addField(di, x);
return bac.getBytes();
}
/** Quick conversion utility method, for use by code generators. (null safe, avoids double copying of the result) */
public static ByteArray marshalAsByteArray(ObjectReference di, BonaPortable x) {
if (x == null)
return null; // consistent with the other methods: f(null) = null // ByteArray.ZERO_BYTE_ARRAY;
ByteArrayComposer bac = new ByteArrayComposer();
bac.addField(di, x);
return new ByteArray(bac.getBuffer(), 0, bac.getLength());
}
/** Creates a new ByteArrayComposer, using this classes static default Charset **/
public ByteArrayComposer() {
this(ObjectReuseStrategy.defaultStrategy);
}
/** Creates a new ByteArrayComposer, using this classes static default Charset **/
public ByteArrayComposer(ObjectReuseStrategy reuseStrategy) {
switch (reuseStrategy) {
case BY_CONTENTS:
this.objectCache = new HashMap<BonaCustom,Integer>(250);
this.useCache = true;
break;
case BY_REFERENCE:
this.objectCache = new IdentityHashMap<BonaCustom,Integer>(250);
this.useCache = true;
break;
default:
this.objectCache = null;
this.useCache = false;
break;
}
this.work = new ByteBuilder(0, getDefaultCharset());
numberOfObjectsSerialized = 0;
numberOfObjectReuses = 0;
}
protected int getNumberOfObjectsSerialized() {
return numberOfObjectsSerialized;
}
@Override
public void setCharset(Charset charset) {
super.setCharset(charset);
// also tell the ByteBuilder!
work.setCharset(charset);
}
/** Sets the current length to 0, allowing reuse of the allocated output buffer for a new message. */
@Override
public void reset() {
work.setLength(0);
numberOfObjectsSerialized = 0;
numberOfObjectReuses = 0;
if (useCache)
objectCache.clear();
}
/** Returns the number of bytes written. */
@Override
public int getLength() { // obtain the number of written bytes (composer)
return work.length();
}
/** Returns the current buffer as a Java byte array. Only the first <code>getLength()</code> bytes of this buffer are valid. */
@Override
public byte[] getBuffer() {
if (LOGGER.isDebugEnabled())
LOGGER.debug("Buffer retrieved, {} bytes written, {} object reuses", getLength(), numberOfObjectReuses);
return work.getCurrentBuffer();
}
/** returns the result as a deep copy byte array of precise length of the result. */
@Override
public byte[] getBytes() {
if (LOGGER.isDebugEnabled())
LOGGER.debug("Bytes retrieved, {} bytes written, {} object reuses", getLength(), numberOfObjectReuses);
return work.getBytes(); // slow!
}
/** allows to add raw data to the produced byte array. Use this for protocol support at beginning or end of a message */
public void addRawData(byte [] data) {
work.write(data);
}
/* *************************************************************************************************
* Serialization goes here
**************************************************************************************************/
// the following two methods are provided as separate methods instead of
// code the single command each time,
// with the intention that they max become extended or redefined and reused
// for CSV output to files with
// customized separators.
// Because this class is defined as final, I hope the JIT will inline them
// for better performance
// THIS IS REQUIRED ONLY LOCALLY
private void terminateField() {
work.append(FIELD_TERMINATOR);
}
protected void writeNull() {
work.append(NULL_FIELD);
}
@Override
public void writeNull(FieldDefinition di) {
work.append(NULL_FIELD);
}
@Override
public void writeNullCollection(FieldDefinition di) {
work.append(NULL_FIELD);
}
@Override
public void startTransmission() {
work.append(TRANSMISSION_BEGIN);
writeNull(); // blank version number
}
@Override
public void terminateTransmission() {
work.append(TRANSMISSION_TERMINATOR);
work.append(TRANSMISSION_TERMINATOR2);
}
@Override
public void terminateRecord() {
if (getWriteCRs()) {
work.append(RECORD_OPT_TERMINATOR);
}
work.append(RECORD_TERMINATOR);
}
@Override
public void writeSuperclassSeparator() {
work.append(PARENT_SEPARATOR);
}
@Override
public void startRecord() {
work.append(RECORD_BEGIN);
writeNull(); // blank version number
}
private void addCharSub(int c) {
if ((c < ' ') && (c != '\t')) {
work.append(ESCAPE_CHAR);
work.append((byte)(c + '@'));
} else if (c <= 127) {
// ASCII character: this is faster
work.append((byte)c);
} else {
work.appendUnicode(c);
}
}
// field type specific output functions
// character
@Override
public void addField(MiscElementaryDataItem di, char c) {
addCharSub(c);
terminateField();
}
protected void unicodeOut(String s) {
// take care not to break multi-Sequences
for (int i = 0; i < s.length();) {
int c = s.codePointAt(i);
addCharSub(c);
i += Character.charCount(c);
}
terminateField();
}
// ascii only (unicode uses different method). This one does a validity check now.
@Override
public void addField(AlphanumericElementaryDataItem di, String s) {
if (s != null) {
if (di.getRestrictToAscii()) {
// don't trust them!
work.append(FixASCII.checkAsciiAndFixIfRequired(s, di.getLength(), di.getName()));
terminateField();
} else {
unicodeOut(s);
}
} else {
writeNull();
}
}
// decimal
@Override
public void addField(NumericElementaryDataItem di, BigDecimal n) {
if (n != null) {
work.appendAscii(n.toPlainString());
terminateField();
} else {
writeNull();
}
}
// byte
@Override
public void addField(BasicNumericElementaryDataItem di, byte n) {
work.appendAscii(Byte.toString(n));
terminateField();
}
// short
@Override
public void addField(BasicNumericElementaryDataItem di, short n) {
work.appendAscii(Short.toString(n));
terminateField();
}
// integer
@Override
public void addField(BasicNumericElementaryDataItem di, int n) {
work.appendAscii(Integer.toString(n));
terminateField();
}
// int(n)
@Override
public void addField(BasicNumericElementaryDataItem di, BigInteger n) {
if (n != null) {
work.appendAscii(n.toString());
terminateField();
} else {
writeNull();
}
}
// long
@Override
public void addField(BasicNumericElementaryDataItem di, long n) {
work.appendAscii(Long.toString(n));
terminateField();
}
// boolean
@Override
public void addField(MiscElementaryDataItem di, boolean b) {
if (b) {
work.append((byte) '1');
} else {
work.append((byte) '0');
}
terminateField();
}
// UUID
@Override
public void addField(MiscElementaryDataItem di, UUID n) {
if (n != null) {
work.appendAscii(n.toString());
terminateField();
} else {
writeNull();
}
}
// float
@Override
public void addField(BasicNumericElementaryDataItem di, float f) {
work.appendAscii(Float.toString(f));
terminateField();
}
// double
@Override
public void addField(BasicNumericElementaryDataItem di, double d) {
work.appendAscii(Double.toString(d));
terminateField();
}
// ByteArray: initial quick & dirty implementation
@Override
public void addField(BinaryElementaryDataItem di, ByteArray b) {
if (b != null) {
b.appendBase64(work);
terminateField();
} else {
writeNull();
}
}
// raw
@Override
public void addField(BinaryElementaryDataItem di, byte[] b) {
if (b != null) {
Base64.encodeToByte(work, b, 0, b.length);
terminateField();
} else {
writeNull();
}
}
// append a left padded ASCII String
private void lpad(String s, int length, byte padCharacter) {
int l = s.length();
while (l++ < length) {
work.append(padCharacter);
}
work.appendAscii(s);
}
// converters for DAY und TIMESTAMP
@Override
public void addField(TemporalElementaryDataItem di, LocalDate t) {
if (t != null) {
int [] values = t.getValues(); // 3 values: year, month, day
int tmpValue = (10000 * values[0]) + (100 * values[1]) + values[2];
// int tmpValue = 10000 * t.getYear() + 100 * t.getMonthOfYear() + t.getDayOfMonth();
work.appendAscii(Integer.toString(tmpValue));
terminateField();
} else {
writeNull();
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalDateTime t) {
if (t != null) {
int [] values = t.getValues(); // 4 values: year, month, day, millis of day
//int tmpValue = 10000 * t.getYear() + 100 * t.getMonthOfYear() + t.getDayOfMonth();
work.appendAscii(Integer.toString((10000 * values[0]) + (100 * values[1]) + values[2]));
int length = di.getFractionalSeconds();
if (length >= 0) {
// not only day, but also time
//tmpValue = 10000 * t.getHourOfDay() + 100 * t.getMinuteOfHour() + t.getSecondOfMinute();
if (length > 0 ? (values[3] != 0) : ((values[3] / 1000) != 0)) {
work.append((byte) '.');
if (di.getHhmmss()) {
int tmpValue = values[3] / 60000; // minutes and hours
tmpValue = (100 * (tmpValue / 60)) + (tmpValue % 60);
lpad(Integer.toString((tmpValue * 100) + ((values[3] % 60000) / 1000)), 6, (byte) '0');
} else {
lpad(Integer.toString(values[3] / 1000), 6, (byte) '0');
}
if (length > 0) {
// add milliseconds
int milliSeconds = values[3] % 1000;
if (milliSeconds != 0) {
lpad(Integer.toString(milliSeconds), 3, (byte) '0');
}
}
}
}
terminateField();
} else {
writeNull();
}
}
@Override
public void addField(TemporalElementaryDataItem di, Instant t) {
if (t != null) {
long millis = t.getMillis();
work.appendAscii(Long.toString(millis / 1000L));
int length = di.getFractionalSeconds();
int millisecs = (int)(millis % 1000L);
if (length > 0 && millisecs != 0) {
work.append((byte)'.');
lpad(Integer.toString(millisecs), 3, (byte)'0');
}
terminateField();
} else {
writeNull();
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalTime t) {
if (t != null) {
int length = di.getFractionalSeconds();
int millis = t.getMillisOfDay();
if (di.getHhmmss()) {
int tmpValue = millis / 60000; // minutes and hours
tmpValue = (100 * (tmpValue / 60)) + (tmpValue % 60);
work.appendAscii(Integer.toString((tmpValue * 100) + ((millis % 60000) / 1000)));
} else {
work.appendAscii(Integer.toString(millis / 1000));
}
if (length > 0 && (millis % 1000) != 0) {
// add milliseconds
work.append((byte)'.');
int milliSeconds = millis % 1000;
lpad(Integer.toString(milliSeconds), 3, (byte)'0');
}
terminateField();
} else {
writeNull();
}
}
@Override
public void startMap(FieldDefinition di, int currentMembers) {
work.append(MAP_BEGIN);
addField(StaticMeta.INTERNAL_INTEGER, di.getMapIndexType().ordinal());
addField(StaticMeta.INTERNAL_INTEGER, currentMembers);
}
@Override
public void startArray(FieldDefinition di, int currentMembers, int sizeOfElement) {
work.append(ARRAY_BEGIN);
addField(StaticMeta.INTERNAL_INTEGER, currentMembers);
}
@Override
public void terminateArray() {
work.append(ARRAY_TERMINATOR);
}
@Override
public void terminateMap() {
work.append(ARRAY_TERMINATOR);
}
@Override
public void startObject(ObjectReference di, BonaCustom obj) {
work.append(OBJECT_BEGIN);
addField(OBJECT_CLASS, obj.ret$PQON());
addField(REVISION_META, obj.ret$MetaData().getRevision());
}
@Override
public void terminateObject(ObjectReference di, BonaCustom obj) {
work.append(OBJECT_TERMINATOR);
}
// hook for inherited classes
protected void notifyReuse(int referencedIndex) {
}
@Override
public void addField(ObjectReference di, BonaCustom obj) {
if (obj == null) {
writeNull();
} else {
if (useCache) {
Integer previousIndex = objectCache.get(obj);
if (previousIndex != null) {
// reuse this instance
work.append(OBJECT_AGAIN);
addField(StaticMeta.INTERNAL_INTEGER, numberOfObjectsSerialized - previousIndex.intValue() - 1); // 0 is same object as previous, 1 = the one before etc...
++numberOfObjectReuses;
notifyReuse(previousIndex);
return;
}
// add the new object to the cache of known objects
objectCache.put(obj, Integer.valueOf(numberOfObjectsSerialized++));
// fall through
}
// start a new object
startObject(di, obj);
// do all fields (now includes terminator)
obj.serializeSub(this);
// terminate the new object
terminateObject(di, obj);
}
}
// enum with numeric expansion: delegate to Null/Int
@Override
public void addEnum(EnumDataItem di, BasicNumericElementaryDataItem ord, BonaNonTokenizableEnum n) {
if (n == null)
writeNull(ord);
else
addField(ord, n.ordinal());
}
// enum with alphanumeric expansion: delegate to Null/String
@Override
public void addEnum(EnumDataItem di, AlphanumericElementaryDataItem token, BonaTokenizableEnum n) {
if (n == null)
writeNull(token);
else
addField(token, n.getToken());
}
// xenum with alphanumeric expansion: delegate to Null/String
@Override
public void addEnum(XEnumDataItem di, AlphanumericElementaryDataItem token, XEnum<?> n) {
if (n == null)
writeNull(token);
else
addField(token, n.getToken());
}
@Override
public boolean addExternal(ObjectReference di, Object obj) {
return false; // perform conversion by default
}
@Override
public void addField(ObjectReference di, Map<String, Object> obj) {
if (obj == null)
writeNull(di);
else
unicodeOut(BonaparteJsonEscaper.asJson(obj));
}
@Override
public void addField(ObjectReference di, List<Object> obj) throws RuntimeException {
if (obj == null)
writeNull(di);
else
unicodeOut(BonaparteJsonEscaper.asJson(obj));
}
@Override
public void addField(ObjectReference di, Object obj) {
if (obj == null)
writeNull(di);
else
unicodeOut(BonaparteJsonEscaper.asJson(obj));
}
}