/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.struct.serial;
import io.netty.buffer.ByteBuf;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.DateTime;
import divconq.util.Base64;
import divconq.util.TimeUtil;
import divconq.lang.BigDateTime;
import divconq.lang.Memory;
import divconq.lang.chars.Special;
import divconq.lang.chars.Utf8Encoder;
import divconq.struct.CompositeStruct;
import divconq.struct.builder.BuilderInfo;
import divconq.struct.builder.BuilderState;
import divconq.struct.builder.BuilderStateException;
import divconq.struct.builder.ICompositeBuilder;
import divconq.struct.builder.ICompositeOutput;
import divconq.struct.builder.ObjectBuilder;
import divconq.struct.scalar.AnyStruct;
import divconq.struct.scalar.BigDateTimeStruct;
import divconq.struct.scalar.BigIntegerStruct;
import divconq.struct.scalar.BinaryStruct;
import divconq.struct.scalar.BooleanStruct;
import divconq.struct.scalar.DateTimeStruct;
import divconq.struct.scalar.DecimalStruct;
import divconq.struct.scalar.IntegerStruct;
import divconq.struct.scalar.NullStruct;
import divconq.struct.scalar.StringStruct;
public class CompositeToBufferBuilder implements ICompositeBuilder {
protected BuilderInfo cstate = null;
protected List<BuilderInfo> bstate = new ArrayList<BuilderInfo>();
protected boolean complete = false;
protected ByteBuf buffer = null;
public CompositeToBufferBuilder(ByteBuf buffer) {
this.buffer = buffer;
}
@Override
public BuilderState getState() {
return (this.cstate != null) ? this.cstate.State : (this.complete) ? BuilderState.Complete : BuilderState.Ready;
}
@Override
public ICompositeBuilder record(Object... props) throws BuilderStateException {
this.startRecord();
String name = null;
for (Object o : props) {
if (name != null) {
this.field(name, o);
name = null;
}
else {
if (o == null)
throw new BuilderStateException("Null Field Name");
name = o.toString();
}
}
this.endRecord();
return this;
}
@Override
public ICompositeBuilder startRecord() throws BuilderStateException {
// indicate we are in a record
this.cstate = new BuilderInfo(BuilderState.InRecord);
this.bstate.add(cstate);
this.write(Special.StartRec);
return this;
}
@Override
public ICompositeBuilder endRecord() throws BuilderStateException {
// cannot call end rec with being in a record or field
if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
throw new BuilderStateException("Cannot end record when in list");
// if in a field, finish it
if (this.cstate.State == BuilderState.InField)
this.endField();
this.write(Special.EndRec);
// return to parent
this.popState();
// mark the value complete, let parent container know we need commas
this.completeValue();
return this;
}
// names may contain only alpha-numerics
@Override
public ICompositeBuilder field(String name, Object value) throws BuilderStateException {
this.field(name);
this.value(value);
return this;
}
/*
* OK to call if in an unnamed field already (then adds name to the field)
* or to call if in a record straight up
*/
@Override
public ICompositeBuilder field(String name) throws BuilderStateException {
// fields cannot occur outside of records
if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
throw new BuilderStateException("Cannot add field while in list");
// if in a named field, finish it
if ((this.cstate.State == BuilderState.InField) && this.cstate.IsNamed)
this.endField();
// if not yet in a field mark as such
if (this.cstate.State == BuilderState.InRecord)
this.field();
this.value(name);
return this;
}
@Override
public ICompositeBuilder field() throws BuilderStateException {
// fields cannot occur outside of records
if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
throw new BuilderStateException("Cannot add field when in list");
// if already in field then pop out of it
if (this.cstate.State == BuilderState.InField)
this.endField();
// if pop leaves us hanging or not in record then bad
if ((this.cstate == null) || (this.cstate.State != BuilderState.InRecord))
throw new BuilderStateException("Cannot end field when not in record");
// note that we are in a field now, value not completed
this.cstate = new BuilderInfo(BuilderState.InField);
this.bstate.add(cstate);
this.write(Special.Field);
return this;
}
private void endField() throws BuilderStateException {
// cannot occur outside of field
if ((this.cstate == null) || (this.cstate.State == BuilderState.InList) || (this.cstate.State == BuilderState.InRecord))
throw new BuilderStateException("Cannot end field when not in a field");
// return to the record state
this.popState();
}
@Override
public ICompositeBuilder list(Object... props) throws BuilderStateException {
this.startList();
for (Object o : props)
this.value(o);
this.endList();
return this;
}
@Override
public ICompositeBuilder startList() throws BuilderStateException {
// mark that we are in a list
this.cstate = new BuilderInfo(BuilderState.InList);
this.bstate.add(cstate);
this.write(Special.StartList);
return this;
}
@Override
public ICompositeBuilder endList() throws BuilderStateException {
// must be in a list
if ((this.cstate == null) || (this.cstate.State != BuilderState.InList))
throw new BuilderStateException("Not in a list, cannot end list");
// end list
this.write(Special.EndList);
// return to parent state
this.popState();
// mark the value complete, let parent container know we need commas
this.completeValue();
return this;
}
@Override
public boolean needFieldName() {
if (this.cstate == null)
return false;
return ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed);
}
@Override
public ICompositeBuilder value(Object value) throws BuilderStateException {
// cannot occur outside of field or list
if ((this.cstate == null) || (this.cstate.State == BuilderState.InRecord))
throw new BuilderStateException("Cannot add a value unless in a field or in a list");
if ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed) {
// TODO check that name is valid
this.write(value.toString());
this.cstate.IsNamed = true;
return this;
}
// TODO handle other object types - reader, etc
if (value instanceof AnyStruct)
value = ((AnyStruct)value).getValue();
// if object can handle it's own ouput then go ahead and use that
if (value instanceof ICompositeOutput) {
((ICompositeOutput)value).toBuilder(this);
this.completeValue();
return this;
}
// if not a composite then it is a scalar
this.write(Special.Scalar);
if (value instanceof BooleanStruct)
value = ((BooleanStruct)value).getValue();
else if (value instanceof IntegerStruct)
value = ((IntegerStruct)value).getValue();
else if (value instanceof BigIntegerStruct)
value = ((BigIntegerStruct)value).getValue();
else if (value instanceof DecimalStruct)
value = ((DecimalStruct)value).getValue();
else if (value instanceof DateTimeStruct)
value = ((DateTimeStruct)value).getValue();
else if (value instanceof BigDateTimeStruct)
value = ((BigDateTimeStruct)value).getValue();
else if (value instanceof BinaryStruct)
value = ((BinaryStruct)value).getValue();
else if (value instanceof StringStruct)
value = ((StringStruct)value).getValue();
if ((value == null) || (value instanceof NullStruct))
this.write("null");
else if (value instanceof Boolean)
this.write(value.toString());
else if ((value instanceof BigDecimal) || (value instanceof Double) || (value instanceof Float))
this.write("(" + value.toString());
else if (value instanceof Number)
this.write(")" + value.toString());
else if (value instanceof DateTime)
this.write("@" + TimeUtil.stampFmt.print((DateTime)value));
else if (value instanceof BigDateTime)
this.write("$" + ((BigDateTime)value).toString());
else if (value instanceof ByteBuffer)
this.write("%" + Base64.encodeToString(((ByteBuffer)value).array(), false));
else if (value instanceof ByteBuf)
this.write("%" + Base64.encodeToString(((ByteBuf)value).array(), false));
else if (value instanceof Memory)
this.write("%" + Base64.encodeToString(((Memory)value).toArray(), false));
else if (value instanceof byte[])
this.write("%" + Base64.encodeToString(((byte[])value), false));
else
this.writeEscape("`" + value.toString()); // TODO more efficient with memory/builder/etc
return this;
}
@Override
// TODO not supported currently, need some sort of escaping/framing
public ICompositeBuilder rawJson(Object value) throws BuilderStateException {
// cannot occur outside of field or list
if ((this.cstate == null) || (this.cstate.State == BuilderState.InRecord))
throw new BuilderStateException("Cannot add JSON when not in field or in list");
if ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed)
throw new BuilderStateException("Cannot use JSON for field name");
// TODO handle other object types - reader, etc
//this.write(value.toString()); // TODO more efficient
throw new BuilderStateException("Cannot use JSON with this builder at this time");
// mark the value complete, let parent container know we need commas
//this.completeValue();
}
public void writeEscape(String str) {
str = str.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t");
this.write(str);
}
public void writeEscape(char ch) {
switch (ch) {
case '\\':
this.write("\\\\");
break;
case '\n':
this.write("\\n");
break;
case '\t':
this.write("\\t");
break;
default:
this.writeChar(ch);
}
}
private void completeValue() throws BuilderStateException {
// if parent, mark it a having a complete value
if (this.cstate != null) {
if (this.cstate.State == BuilderState.InField) // be sure to pop the state
this.endField();
}
}
private void popState() throws BuilderStateException {
if (this.cstate == null)
throw new BuilderStateException("Cannot pop state when state is null");
this.bstate.remove(this.bstate.size() - 1);
if (this.bstate.size() == 0) {
this.cstate = null;
this.complete = true;
}
else
this.cstate = this.bstate.get(this.bstate.size() - 1);
}
/**
* Write a single character into Memory as UTF-8. Increment position accordingly.
*
* @param ch character to write
*/
public void writeChar(int ch) {
this.buffer.writeBytes(Utf8Encoder.encode(ch));
}
/**
* Write a special character into Memory as UTF-8. Increment position accordingly.
*
* @param ch character to write
*/
public void write(Special ch) {
this.buffer.writeBytes(Utf8Encoder.encode(ch.getCode()));
}
/**
* Write a string into Memory as UTF-8. Increment position accordingly.
*
* @param str string to write
*/
public void write(CharSequence str) {
// TODO this could be more efficient - encode <= 64 bytes at a time and add
this.buffer.writeBytes(Utf8Encoder.encode(str));
}
@Override
public Memory toMemory() {
return new Memory(this.buffer.array());
}
@Override
public CompositeStruct toLocal() {
this.buffer.readerIndex(0);
ObjectBuilder obj = new ObjectBuilder();
BufferToCompositeParser parser = new BufferToCompositeParser(obj);
try {
parser.parseStruct(this.buffer);
}
catch (Exception x) {
}
return obj.getRoot();
}
}