/*
* 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.Externalizable;
import java.io.IOException;
import java.io.ObjectOutput;
import java.math.BigDecimal;
import java.math.BigInteger;
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 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.enums.XEnum;
import de.jpaw.util.ByteArray;
/**
* The ExternalizableComposer class.
*
* @author Michael Bischoff
* @version $Revision$
*
* Defines some constants and static utility methods for the Externalizable interface.
* About null checking, the principle is that boxed primitive types must be null-checked and possibly unboxed in
* the class itself (the caller), but any object which does not exist in primitive form has its null check here.
*/
public class ExternalizableComposer extends AbstractMessageComposer<IOException> implements ExternalizableConstants {
private final ObjectOutput out;
public ExternalizableComposer(ObjectOutput out) {
this.out = out;
}
// entry called from generated objects: (Object header has been written already by internal methods (and unfortunately in some different fashion...))
public static void serialize(BonaCustom obj, ObjectOutput _out) throws IOException {
MessageComposer<IOException> _w = new ExternalizableComposer(_out);
obj.serializeSub(_w);
_w.terminateObject(StaticMeta.OUTER_BONAPORTABLE, obj);
}
@Override
public void writeNull(FieldDefinition di) throws IOException {
out.writeByte(NULL_FIELD);
}
@Override
public void writeNullCollection(FieldDefinition di) throws IOException {
out.writeByte(NULL_FIELD);
}
/**
* Primitives
* */
@Override
public void addField(BasicNumericElementaryDataItem di, double d) throws IOException {
out.writeByte(BINARY_DOUBLE);
out.writeDouble(d);
}
@Override
public void addField(BasicNumericElementaryDataItem di, float f) throws IOException {
out.writeByte(BINARY_FLOAT);
out.writeFloat(f);
}
@Override
public void addField(MiscElementaryDataItem di, UUID n) throws IOException {
if (n == null) {
out.writeByte(NULL_FIELD);
} else {
out.writeByte(TEXT);
out.writeUTF(n.toString());
}
}
@Override
public void addField(AlphanumericElementaryDataItem di, String s) throws IOException {
if (s == null) {
out.writeByte(NULL_FIELD);
} else {
out.writeByte(TEXT);
out.writeUTF(s);
}
}
@Override
public void addField(BinaryElementaryDataItem di, ByteArray b) throws IOException {
if (b == null) {
out.writeByte(NULL_FIELD);
} else {
out.writeByte(BINARY);
b.writeExternal(out);
}
}
@Override
public void addField(BinaryElementaryDataItem di, byte[] b) throws IOException {
if (b == null) {
out.writeByte(NULL_FIELD);
} else {
out.writeByte(BINARY);
ByteArray.writeBytes(out, b, 0, b.length);
}
}
@Override
public void addField(MiscElementaryDataItem di, char c) throws IOException {
out.writeByte(TEXT);
out.writeUTF(String.valueOf(c));
}
@Override
public void addField(MiscElementaryDataItem di, boolean b) throws IOException {
out.writeByte(b ? INT_ONE : INT_ZERO);
}
@Override
public void addField(NumericElementaryDataItem di, BigDecimal n) throws IOException {
if (n == null) {
out.writeByte(NULL_FIELD);
} else {
int scale = n.scale();
if ((scale < 0) || (scale > 18)) {
throw new IOException("cannot convert BigDecimal with negative scale or scale > 18: " + scale);
}
long fraction = n.unscaledValue().longValue() % powersOfTen[scale];
if (fraction != 0) {
out.writeByte(FRAC_SCALE_0 + scale);
writeVarLong(fraction);
}
writeVarLong(n.longValue());
}
}
// the following method will be used for byte, short, int
private void writeVarInt(int i) throws IOException {
if ((i >= -1) && (i <= 16)) {
out.writeByte(i + INT_ZERO);
} else if ((i >= -128) && (i <= 127)) {
out.writeByte(INT_ONEBYTE);
out.writeByte(i);
} else if ((i >= -32768) && (i <= 32767)) {
out.writeByte(INT_TWOBYTES);
out.writeShort(i);
} else {
out.writeByte(INT_FOURBYTES);
out.writeInt(i);
}
}
// the following method will be used for byte, short, int
@SuppressWarnings("cast")
private void writeVarLong(long l) throws IOException {
if ((long)(int)l == l) {
writeVarInt((int)l);
} else {
out.writeByte(INT_EIGHTBYTES);
out.writeLong(l);
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalDate t) throws IOException {
if (t == null) {
out.writeByte(NULL_FIELD);
} else {
int [] values = t.getValues(); // 3 values: year, month, day
writeVarInt((10000 * values[0]) + (100 * values[1]) + values[2]);
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalDateTime t) throws IOException {
if (t == null) {
out.writeByte(NULL_FIELD);
} else {
int [] values = t.getValues(); // 4 values: year, month, day, milliseconds
if (values[3] != 0) {
// fractional part first...
out.writeByte(FRAC_SCALE_0 + 9);
if (di.getHhmmss()) {
// convert milliseconds to hhmmssfff format
int tmp = values[3] / 60000; // number of minutes
tmp = ((tmp / 60) * 100) + (tmp % 60);
writeVarInt((tmp * 100000) + (values[3] % 60000));
} else {
writeVarInt(values[3]);
}
}
// then integral part
writeVarInt((10000 * values[0]) + (100 * values[1]) + values[2]);
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalTime t) throws IOException {
if (t == null) {
out.writeByte(NULL_FIELD);
} else {
int millis = t.getMillisOfDay();
out.writeByte(FRAC_SCALE_0 + 9);
if (di.getHhmmss()) {
// convert milliseconds to hhmmssfff format
int tmp = millis / 60000; // number of minutes
tmp = ((tmp / 60) * 100) + (tmp % 60);
writeVarInt((tmp * 100000) + (millis % 60000));
} else {
writeVarInt(millis);
}
}
}
@Override
public void addField(TemporalElementaryDataItem di, Instant t) throws IOException {
if (t == null) {
out.writeByte(NULL_FIELD);
} else {
long millis = t.getMillis();
writeVarLong(millis);
}
}
static public void writeObject(ObjectOutput out, BonaCustom obj) throws IOException {
if (obj == null) {
out.writeByte(NULL_FIELD);
} else {
// do not rely on Java logic, we know the object is BonaCustom and call the externalizer interface directly
// do we really? (At the moment it's optional)
out.writeByte(OBJECT_BEGIN);
if (nestedObjectsInternally) {
out.writeUTF(obj.ret$PQON());
out.writeLong(obj.ret$MetaData().getSerialUID());
((Externalizable)obj).writeExternal(out); // TODO: obj.deserialize(this);
} else {
out.writeObject(obj); // so fall back to normal behaviour!
}
}
}
@Override
public void startMap(FieldDefinition di, int currentMembers) throws IOException {
out.writeByte(MAP_BEGIN);
writeVarInt(di.getMapIndexType().ordinal());
writeVarInt(currentMembers);
}
@Override
public void startArray(FieldDefinition di, int currentMembers, int sizeOfElement) throws IOException {
out.writeByte(ARRAY_BEGIN);
writeVarInt(currentMembers);
}
/* the following do not really apply at object serialization level, but are provided
* in order to be able to extend this composer to other tasks
* @see de.jpaw.bonaparte.core.MessageComposer#startTransmission()
*/
@Override
public void startTransmission() throws IOException {
out.write(TRANSMISSION_BEGIN);
writeNull(null); // blank version number
}
// public void startObject(String name, String version) throws IOException {
// // TODO Auto-generated method stub
//
// }
@Override
public void writeSuperclassSeparator() throws IOException {
out.writeByte(PARENT_SEPARATOR);
}
@Override
public void terminateArray() throws IOException {
out.writeByte(ARRAY_TERMINATOR);
}
@Override
public void terminateMap() throws IOException {
out.writeByte(ARRAY_TERMINATOR);
}
@Override
public void terminateRecord() throws IOException {
out.write(RECORD_TERMINATOR);
}
@Override
public void terminateTransmission() throws IOException {
out.write(TRANSMISSION_TERMINATOR);
out.write(TRANSMISSION_TERMINATOR2);
}
@Override
public void startRecord() throws IOException {
out.write(RECORD_BEGIN);
writeNull(null); // blank version number
}
@Override
public void addField(BasicNumericElementaryDataItem di, byte n) throws IOException {
writeVarInt(n);
}
@Override
public void addField(BasicNumericElementaryDataItem di, short n) throws IOException {
writeVarInt(n);
}
@Override
public void addField(BasicNumericElementaryDataItem di, long n) throws IOException {
writeVarLong(n);
}
@Override
public void addField(BasicNumericElementaryDataItem di, int n) throws IOException {
writeVarInt(n);
}
@Override
public void addField(BasicNumericElementaryDataItem di, BigInteger n) throws IOException {
if (n == null) {
out.writeByte(NULL_FIELD);
} else {
// write it as a byte array: an improved version would "steal" the existing internal byte Array using reflection, instead of copying the stuff
out.writeByte(BINARY);
byte [] tmp = n.toByteArray();
ByteArray.writeBytes(out, tmp, 0, tmp.length);
}
}
@Override
public void startObject(ObjectReference di, BonaCustom obj) throws IOException {
}
@Override
public void terminateObject(ObjectReference di, BonaCustom obj) throws IOException {
out.writeByte(OBJECT_TERMINATOR);
}
@Override
public void addField(ObjectReference di, BonaCustom obj) throws IOException {
if (obj == null) {
out.writeByte(NULL_FIELD);
} else {
// do not rely on Java logic, we know the object is BonaCustom and call the externalizer interface directly
// do we really? (At the moment it's optional)
startObject(di, obj);
out.writeByte(OBJECT_BEGIN); // this logically belongs to the lines below, do not split here!
if (nestedObjectsInternally) {
out.writeUTF(obj.ret$PQON());
addField(REVISION_META, obj.ret$MetaData().getRevision());
obj.serializeSub(this);
} else {
out.writeObject(obj); // so fall back to normal behaviour!
}
terminateObject(di, obj);
}
}
// enum with numeric expansion: delegate to Null/Int
@Override
public void addEnum(EnumDataItem di, BasicNumericElementaryDataItem ord, BonaNonTokenizableEnum n) throws IOException {
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) throws IOException {
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) throws IOException {
if (n == null)
writeNull(token);
else
addField(token, n.getToken());
}
@Override
public boolean addExternal(ObjectReference di, Object obj) throws IOException {
return false; // perform conversion by default
}
@Override
public void addField(ObjectReference di, Map<String, Object> obj) throws IOException {
if (obj == null)
writeNull(di);
else {
out.writeByte(TEXT);
out.writeUTF(BonaparteJsonEscaper.asJson(obj));
}
}
@Override
public void addField(ObjectReference di, List<Object> obj) throws IOException {
if (obj == null)
writeNull(di);
else {
out.writeByte(TEXT);
out.writeUTF(BonaparteJsonEscaper.asJson(obj));
}
}
@Override
public void addField(ObjectReference di, Object obj) throws IOException {
if (obj == null)
writeNull(di);
else {
out.writeByte(TEXT);
out.writeUTF(BonaparteJsonEscaper.asJson(obj));
}
}
}