/**
* == @Spearal ==>
*
* Copyright (C) 2014 Franck WOLFF & William DRAI (http://www.spearal.io)
*
* 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 org.spearal.impl;
import static org.spearal.impl.SharedConstants.BIG_NUMBER_ALPHA_MIRROR;
import static org.spearal.impl.SharedConstants.UTF8;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import org.spearal.SpearalContext;
import org.spearal.SpearalPropertyFilter;
import org.spearal.configuration.FilteredBeanDescriptorFactory.FilteredBeanDescriptor;
import org.spearal.configuration.PropertyFactory.Property;
import org.spearal.impl.cache.IdentityIndexMap;
import org.spearal.impl.cache.StringIndexMap;
/**
* @author Franck WOLFF
*/
public class SpearalEncoderImpl implements ExtendedSpearalEncoder {
private final SpearalContext context;
private final SpearalPropertyFilter propertyFilter;
private final OutputStream out;
private final StringIndexMap sharedStrings;
private final IdentityIndexMap sharedObjects;
private final Map<Class<?>, FilteredBeanDescriptor> descriptors;
private final byte[] buffer;
private int position;
private int depth;
public SpearalEncoderImpl(SpearalContext context, OutputStream out) {
this(context, null, out, 1024);
}
public SpearalEncoderImpl(SpearalContext context, OutputStream out, int capacity) {
this(context, null, out, capacity);
}
public SpearalEncoderImpl(SpearalContext context, SpearalPropertyFilter request, OutputStream out) {
this(context, request, out, 1024);
}
public SpearalEncoderImpl(final SpearalContext context, SpearalPropertyFilter propertyFilter, OutputStream out, int capacity) {
this.context = context;
this.propertyFilter = (propertyFilter != null ? propertyFilter : new SpearalPropertyFilterImpl(context));
this.out = out;
this.sharedStrings = new StringIndexMap();
this.sharedObjects = new IdentityIndexMap();
this.descriptors = new IdentityHashMap<Class<?>, FilteredBeanDescriptor>(32);
this.buffer = new byte[capacity];
this.position = 0;
this.depth = 0;
}
@Override
public SpearalContext getContext() {
return context;
}
@Override
public SpearalPropertyFilter getPropertyFilter() {
return propertyFilter;
}
@Override
public void writeAny(Object o) throws IOException {
++depth;
if (o == null)
writeNull();
else
context.getCoder(o.getClass()).encode(this, o);
if ((--depth) == 0)
flushBuffer();
}
@Override
public void writeNull() throws IOException {
ensureCapacity(1);
buffer[position++] = (byte)SpearalType.NULL.id();
}
@Override
public void writeBoolean(boolean value) throws IOException {
ensureCapacity(1);
buffer[position++] = (byte)(value ? SpearalType.TRUE.id() : SpearalType.FALSE.id());
}
@Override
public void writeDateTime(SpearalDateTime date) throws IOException {
int parameters = 0x00;
int subsecs = date.nanoseconds;
if (date.hasDate)
parameters |= 0x08;
if (date.hasTime) {
parameters |= 0x04;
if (subsecs != 0) {
if (subsecs % 1000 == 0) {
if (subsecs % 1000000 == 0) {
subsecs /= 1000000;
parameters |= 0x03;
}
else {
subsecs /= 1000;
parameters |= 0x02;
}
}
else
parameters |= 0x01;
}
}
ensureCapacity(1);
buffer[position++] = (byte)(SpearalType.DATE_TIME.id() | parameters);
if (date.hasDate) {
int year = date.year - 2000;
int inverse;
if (year < 0) {
inverse = 0x80;
year = -year;
}
else
inverse = 0x00;
int length0 = unsignedIntLength0(year);
ensureCapacity(length0 + 3);
buffer[position++] = (byte)(inverse | (length0 << 4) | date.month);
buffer[position++] = (byte)date.date;
writeUnsignedIntValue(year, length0);
}
if (date.hasTime) {
if (subsecs == 0) {
ensureCapacity(3);
buffer[position++] = (byte)date.hours;
buffer[position++] = (byte)date.minutes;
buffer[position++] = (byte)date.seconds;
}
else {
int length0 = unsignedIntLength0(subsecs);
ensureCapacity(length0 + 4);
buffer[position++] = (byte)((length0 << 5) | date.hours);
buffer[position++] = (byte)date.minutes;
buffer[position++] = (byte)date.seconds;
writeUnsignedIntValue(subsecs, length0);
}
}
}
@Override
public void writeByte(byte value) throws IOException {
int inverse = 0;
if (value < 0) {
inverse = 0x08;
if (value != Byte.MIN_VALUE)
value = (byte)-value;
}
ensureCapacity(2);
buffer[position++] = (byte)(SpearalType.INTEGRAL.id() | inverse);
buffer[position++] = value;
}
@Override
public void writeShort(short value) throws IOException {
int inverse;
int length0;
if (value < 0) {
inverse = 0x08;
if (value == Short.MIN_VALUE)
length0 = 1;
else {
value = (short)-value;
length0 = (value <= 0xff ? 0 : 1);
}
}
else {
inverse = 0;
length0 = (value <= 0xff ? 0 : 1);
}
ensureCapacity(length0 + 2);
buffer[position++] = (byte)(SpearalType.INTEGRAL.id() | inverse | length0);
if (length0 == 1)
buffer[position++] = (byte)(value >>> 8);
buffer[position++] = (byte)value;
}
@Override
public void writeInt(int value) throws IOException {
int inverse;
int length0;
if (value < 0) {
inverse = 0x08;
if (value == Integer.MIN_VALUE)
length0 = 3;
else {
value = -value;
length0 = unsignedIntLength0(value);
}
}
else {
inverse = 0;
length0 = unsignedIntLength0(value);
}
ensureCapacity(length0 + 2);
buffer[position++] = (byte)(SpearalType.INTEGRAL.id() | inverse | length0);
writeUnsignedIntValue(value, length0);
}
@Override
public void writeLong(long value) throws IOException {
int inverse = 0;
int length0;
if (value < 0) {
if (value == Long.MIN_VALUE)
length0 = 7;
else {
inverse = 0x08;
value = -value;
length0 = unsignedLongLength0(value);
}
}
else
length0 = unsignedLongLength0(value);
ensureCapacity(length0 + 2);
final byte[] buffer = this.buffer;
int position = this.position;
buffer[position++] = (byte)(SpearalType.INTEGRAL.id() | inverse | length0);
switch (length0) {
case 7:
buffer[position++] = (byte)(value >>> 56);
case 6:
buffer[position++] = (byte)(value >>> 48);
case 5:
buffer[position++] = (byte)(value >>> 40);
case 4:
buffer[position++] = (byte)(value >>> 32);
case 3:
buffer[position++] = (byte)(value >>> 24);
case 2:
buffer[position++] = (byte)(value >>> 16);
case 1:
buffer[position++] = (byte)(value >>> 8);
case 0:
buffer[position++] = (byte)value;
break;
default:
throw new RuntimeException("Internal error: length0=" + length0);
}
this.position = position;
}
@Override
public void writeBigInteger(BigInteger value) throws IOException {
writeBigNumberData(SpearalType.BIG_INTEGRAL.id(), exponentize(value));
}
@Override
public void writeFloat(float value) throws IOException {
writeDouble(value);
}
@Override
public void writeDouble(double value) throws IOException {
long bits = Double.doubleToLongBits(value);
// Not NaN, +/-Infinity or -0.0
if ((bits & 0x7ff0000000000000L) != 0x7ff0000000000000L && bits != 0x8000000000000000L) {
long doubleAsLong = (long)value;
if (value == doubleAsLong) {
// 6.5 bytes max (absolute value), i.e. max length of a IEEE 754 fraction part.
if (doubleAsLong >= -0x000fffffffffffffL && doubleAsLong <= 0x000fffffffffffffL) {
writeLong(doubleAsLong);
return;
}
}
else {
doubleAsLong = (long)(value * 1000.0);
if (value == (doubleAsLong / 1000.0) ||
value == ((doubleAsLong += (doubleAsLong < 0 ? -1 : 1)) / 1000.0)) {
// 4 bytes max (absolute value)
if (doubleAsLong >= -0xffffffffL && doubleAsLong <= 0xffffffffL) {
int inverse;
if (doubleAsLong < 0) {
doubleAsLong = -doubleAsLong;
inverse = 0x04;
}
else
inverse = 0x00;
int length0 = unsignedLongLength0(doubleAsLong);
ensureCapacity(length0 + 2);
final byte[] buffer = this.buffer;
int position = this.position;
buffer[position++] = (byte)(SpearalType.FLOATING.id() | 0x08 | inverse | length0);
switch (length0) {
case 3:
buffer[position++] = (byte)(doubleAsLong >>> 24);
case 2:
buffer[position++] = (byte)(doubleAsLong >>> 16);
case 1:
buffer[position++] = (byte)(doubleAsLong >>> 8);
case 0:
buffer[position++] = (byte)doubleAsLong;
break;
default:
throw new RuntimeException("Internal error: length0=" + length0);
}
this.position = position;
return;
}
}
}
}
ensureCapacity(9);
buffer[position++] = (byte)SpearalType.FLOATING.id();
writeLongData(bits);
}
@Override
public void writeBigDecimal(BigDecimal value) throws IOException {
writeBigNumberData(SpearalType.BIG_FLOATING.id(), value.toString());
}
@Override
public void writeChar(char value) throws IOException {
writeStringData(SpearalType.STRING.id(), String.valueOf(value));
}
@Override
public final void writeString(String value) throws IOException {
writeStringData(SpearalType.STRING.id(), value);
}
@Override
public void writeByteArray(byte[] value) throws IOException {
if (!putAndWriteObjectReference(SpearalType.BYTE_ARRAY.id(), value)) {
writeTypeUint(SpearalType.BYTE_ARRAY.id(), value.length);
writeBytes(value);
}
}
@Override
public void writeArray(Object value) throws IOException {
if (!putAndWriteObjectReference(SpearalType.COLLECTION.id(), value)) {
final int size = Array.getLength(value);
writeTypeUint(SpearalType.COLLECTION.id(), size);
for (int i = 0; i < size; i++)
writeAny(Array.get(value, i));
}
}
@Override
public void writeCollection(Collection<?> value) throws IOException {
if (!putAndWriteObjectReference(SpearalType.COLLECTION.id(), value)) {
final int size = value.size();
writeTypeUint(SpearalType.COLLECTION.id(), size);
for (Object item : value)
writeAny(item);
}
}
@Override
public void writeMap(Map<?, ?> value) throws IOException {
if (!putAndWriteObjectReference(SpearalType.MAP.id(), value)) {
final int size = value.size();
writeTypeUint(SpearalType.MAP.id(), size);
for (Map.Entry<?, ?> entry : value.entrySet()) {
writeAny(entry.getKey());
writeAny(entry.getValue());
}
}
}
@Override
public void writeEnum(Enum<?> value) throws IOException {
writeStringData(SpearalType.ENUM.id(), value.getClass().getName());
writeStringData(SpearalType.STRING.id(), value.name());
}
@Override
public void writeClass(Class<?> value) throws IOException {
writeStringData(SpearalType.CLASS.id(), value.getName());
}
@Override
public void writeBean(Object value) throws IOException {
if (!putAndWriteObjectReference(SpearalType.BEAN.id(), value)) {
Class<?> cls = value.getClass();
FilteredBeanDescriptor descriptor = descriptors.get(cls);
if (descriptor == null) {
descriptor = context.createDescriptor(propertyFilter, value);
if (descriptor.isCacheable())
descriptors.put(cls, descriptor);
}
writeStringData(SpearalType.BEAN.id(), descriptor.getDescription());
for (Property property : descriptor.getProperties()) {
if (property == null)
continue;
try {
property.write(this, value);
}
catch (IOException e) {
throw e;
}
catch (Exception e) {
throw new IOException(e);
}
}
}
}
private void writeStringData(int type, String s) throws IOException {
if (s.length() == 0) {
ensureCapacity(2);
buffer[position++] = (byte)type;
buffer[position++] = 0;
return;
}
if (!putAndWriteStringReference(type, s)) {
byte[] bytes = s.getBytes(UTF8);
writeTypeUint(type, bytes.length);
writeBytes(bytes);
}
}
private static String exponentize(BigInteger value) {
String representation = value.toString(10);
final int length = representation.length();
if (length > 3) {
int trailingZeros = 0;
for (int i = length - 1; i > 0 && representation.charAt(i) == '0'; i--)
trailingZeros++;
if (trailingZeros > 2)
representation = representation.substring(0, length - trailingZeros) + "E" + trailingZeros;
}
return representation;
}
private void writeBigNumberData(int type, String representation) throws IOException {
if (!putAndWriteStringReference(type, representation)) {
final int length = representation.length();
writeTypeUint(type, length);
final byte[] buffer = this.buffer;
final int bufferLength = buffer.length;
int position = this.position;
for (int i = 0; i < length; ) {
if (position >= bufferLength) {
this.position = bufferLength;
flushBuffer();
position = 0;
}
int b = (BIG_NUMBER_ALPHA_MIRROR[representation.charAt(i++)] << 4);
if (i == length) {
buffer[position++] = (byte)b;
break;
}
b |= BIG_NUMBER_ALPHA_MIRROR[representation.charAt(i++)];
buffer[position++] = (byte)b;
}
this.position = position;
}
}
private boolean putAndWriteObjectReference(int type, Object o) throws IOException {
int index = sharedObjects.putIfAbsent(o);
if (index != -1) {
writeTypeUint(type | 0x08, index);
return true;
}
return false;
}
private boolean putAndWriteStringReference(int type, String s) throws IOException {
int index = sharedStrings.putIfAbsent(s);
if (index != -1) {
writeTypeUint(type | 0x04, index);
return true;
}
return false;
}
private void writeTypeUint(int type, int uint) throws IOException {
final int length0 = unsignedIntLength0(uint);
ensureCapacity(length0 + 2);
buffer[position++] = (byte)(type | length0);
writeUnsignedIntValue(uint, length0);
}
private static int unsignedLongLength0(long value) {
if (value <= 0xffffffffL) {
if (value <= 0xffffL)
return (value <= 0xffL ? 0 : 1);
return (value <= 0xffffffL ? 2 : 3);
}
if (value <= 0xffffffffffffL)
return (value <= 0xffffffffffL ? 4 : 5);
return (value <= 0xffffffffffffffL ? 6 : 7);
}
private void writeLongData(long value) {
final byte[] buffer = this.buffer;
int position = this.position;
buffer[position++] = (byte)(value >>> 56);
buffer[position++] = (byte)(value >>> 48);
buffer[position++] = (byte)(value >>> 40);
buffer[position++] = (byte)(value >>> 32);
buffer[position++] = (byte)(value >>> 24);
buffer[position++] = (byte)(value >>> 16);
buffer[position++] = (byte)(value >>> 8);
buffer[position++] = (byte)value;
this.position = position;
}
private static int unsignedIntLength0(int value) {
if (value <= 0xff)
return 0;
if (value <= 0xffff)
return 1;
return (value <= 0xffffff ? 2 : 3);
}
private void writeUnsignedIntValue(int value, int length0) {
switch (length0) {
case 3:
buffer[position++] = (byte)(value >>> 24);
case 2:
buffer[position++] = (byte)(value >>> 16);
case 1:
buffer[position++] = (byte)(value >>> 8);
case 0:
buffer[position++] = (byte)value;
break;
default:
throw new RuntimeException("Internal error: length0=" + length0);
}
}
private void ensureCapacity(int capacity) throws IOException {
if (buffer.length - position < capacity)
flushBuffer();
}
private void flushBuffer() throws IOException {
if (position > 0) {
out.write(buffer, 0, position);
position = 0;
}
}
private void writeBytes(byte[] bytes) throws IOException {
if (bytes.length > 0) {
if (buffer.length - position >= bytes.length) {
System.arraycopy(bytes, 0, buffer, position, bytes.length);
position += bytes.length;
}
else {
flushBuffer();
out.write(bytes, 0, bytes.length);
}
}
}
}