/* ************************************************************************
#
# 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.builder;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import divconq.util.Base64;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import divconq.lang.BigDateTime;
import divconq.lang.Memory;
import divconq.struct.CompositeStruct;
import divconq.struct.RecordStruct;
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;
import divconq.xml.XNode;
public class XmlStreamBuilder implements ICompositeBuilder {
protected BuilderInfo cstate = null;
protected List<BuilderInfo> bstate = new ArrayList<BuilderInfo>();
protected boolean complete = false;
protected PrintStream pw = null;
protected boolean pretty = false;
public XmlStreamBuilder(PrintStream pw) {
this.pw = pw;
}
public XmlStreamBuilder(PrintStream pw, boolean pretty) {
this.pw = pw;
this.pretty = pretty;
}
@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 {
// if in a list and need comma
if ((this.cstate != null) && (this.cstate.State == BuilderState.InList) && this.cstate.CommaNeeded) {
//this.write(", ");
if (this.pretty) {
this.write("\n");
this.indent();
}
this.cstate.CommaNeeded = false;
}
// indicate we are in a record
this.cstate = new BuilderInfo(BuilderState.InRecord, (this.cstate != null) ? this.cstate.indent + 1 : 1);
this.bstate.add(cstate);
this.write("<Record>");
if (this.pretty) {
this.write("\n");
this.indent();
}
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();
// return to parent
this.popState();
if (this.pretty) {
this.write("\n");
this.indent();
}
this.write("</Record>");
// 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 when 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");
// we should now be at record level, check for comma state
if (this.cstate.CommaNeeded) {
//this.write(", ");
if (this.pretty) {
this.write("\n");
this.indent();
}
this.cstate.CommaNeeded = false;
}
// note that we are in a field now, value not completed
this.cstate = new BuilderInfo(BuilderState.InField, this.cstate.indent);
this.bstate.add(cstate);
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 field");
// end the field
if (!this.cstate.ValueComplete)
this.write("<Scalar />");
this.write("</Field>");
// return to the record state
this.popState();
// we should now be at record level, mark comma state
this.cstate.CommaNeeded = true;
}
@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 {
// if in a list and need comma
if ((this.cstate != null) && ((this.cstate.State == BuilderState.InList) || (this.cstate.State == BuilderState.InField)) && this.cstate.CommaNeeded) {
//this.write(", ");
if (this.pretty) {
this.write("\n");
this.indent();
}
this.cstate.CommaNeeded = false;
}
// mark that we are in a list
this.cstate = new BuilderInfo(BuilderState.InList, (this.cstate != null) ? this.cstate.indent + 1 : 1);
this.bstate.add(cstate);
// start out complete (an empty list is complete)
this.cstate.ValueComplete = true;
this.write("<List>");
if (this.pretty) {
this.write("\n");
this.indent();
}
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("Cannot end list when not in list");
if (!this.cstate.ValueComplete)
this.endItem();
// return to parent state
this.popState();
if (this.pretty) {
this.write("\n");
this.indent();
}
// end list
this.write("</List>");
// mark the value complete, let parent container know we need commas
this.completeValue();
return this;
}
private void indent() {
if (this.cstate == null)
return;
for (int i = 0; i < this.cstate.indent; i++)
this.writeChar('\t');
}
private void endItem() throws BuilderStateException {
// cannot occur outside of field
if ((this.cstate == null) || (this.cstate.State != BuilderState.InList))
throw new BuilderStateException("Cannot end item when not in list");
// end the item
if (!this.cstate.ValueComplete) {
this.write("<Scalar />");
// tell list that the lastest entry is complete
this.cstate.ValueComplete = true;
}
// note need for comma in parent
this.cstate.CommaNeeded = true;
}
@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.InField) && (this.cstate.State != BuilderState.InList)))
throw new BuilderStateException("Cannot add value unless in field or in list");
if ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed) {
String name = value.toString();
if (!RecordStruct.validateFieldName(name))
throw new BuilderStateException("Invalid field name");
this.write("<Field Name=\"" + name + "\">");
this.cstate.IsNamed = true;
return this;
}
// if in a list, check if we need a comma
if (this.cstate.State == BuilderState.InList) {
if (!this.cstate.ValueComplete)
this.endItem();
this.cstate.ValueComplete = false;
// we should now be at list level, check for comma state
if (this.cstate.CommaNeeded) {
//this.write(", ");
if (this.pretty) {
this.write("\n");
this.indent();
}
this.cstate.CommaNeeded = false;
}
}
// TODO cleanup - handle data more like JsonBuilder
// TODO handle other object types - reader, etc
if (value instanceof AnyStruct)
value = ((AnyStruct)value).getValue();
if (value == null)
this.write("<Scalar />");
else if (value instanceof NullStruct)
this.write("<Scalar />");
else if (value instanceof BooleanStruct)
this.write(((BooleanStruct)value).getValue() ? "<Scalar Value=\"True\" />" : "<Scalar Value=\"False\" />");
else if (value instanceof IntegerStruct)
this.write("<Scalar Value=\"" + ((IntegerStruct)value).getValue() + "\" />");
else if (value instanceof BigIntegerStruct)
this.write("<Scalar Value=\"" + ((BigIntegerStruct)value).getValue() + "\" />");
else if (value instanceof DecimalStruct)
this.write("<Scalar Value=\"" + ((DecimalStruct)value).getValue() + "\" />");
else if (value instanceof DateTimeStruct)
this.write("<Scalar Value=\"" + ((DateTimeStruct)value).toString() + "\" />");
else if (value instanceof BigDateTimeStruct)
this.write("<Scalar Value=\"" + ((DateTimeStruct)value).getValue() + "\" />");
else if (value instanceof BinaryStruct) {
String output = Base64.encodeToString(((BinaryStruct)value).getValue().toArray(), false);
if ((output.length() < 65) && !output.contains("\t") && !output.contains("\n")) {
this.write("<Scalar Value=\"");
this.write(XNode.quote(output)); // TODO more efficient
this.write("\" />");
}
else {
this.write("<Scalar>");
this.write(output);
this.write("</Scalar>");
}
}
else if (value instanceof StringStruct) {
String output = ((StringStruct)value).toString();
if ((output.length() < 65) && !output.contains("\t") && !output.contains("\n")) {
this.write("<Scalar Value=\"");
this.write(XNode.quote(output)); // TODO more efficient
this.write("\" />");
}
else {
this.write("<Scalar>");
this.write(output);
this.write("</Scalar>");
}
}
else if (value instanceof Boolean)
this.write((Boolean)value ? "<Scalar Value=\"True\" />" : "<Scalar Value=\"False\" />");
else if (value instanceof Number)
this.write("<Scalar Value=\"" + value.toString() + "\" />");
else if (value instanceof DateTime)
this.write("<Scalar Value=\"" + ((DateTime)value).toDateTime(DateTimeZone.UTC) + "\" />");
else if (value instanceof BigDateTime)
this.write("<Scalar Value=\"" + value + "\" />");
else if (value instanceof CompositeStruct)
((CompositeStruct)value).toBuilder(this);
else if (value instanceof ICompositeOutput)
this.write(value.toString()); // TODO no, not really - need to work with object (e.g. raw json) this is place holder until we use new parser
//else if (value instanceof ByteBuffer)
//this.write("\"" + Base64.encodeBase64String(((ByteBuffer)value).array()) + "\""); // TODO more efficient
//else if (value instanceof byte[])
//this.write("\"" + Base64.encodeBase64String((byte[])value) + "\""); // TODO more efficient
//else if (value instanceof Elastic)
//((Elastic)value).toBuilder(this);
else {
String output = value.toString();
if ((output.length() < 65) && !output.contains("\t") && !output.contains("\n")) {
this.write("<Scalar Value=\"");
this.write(XNode.quote(output)); // TODO more efficient
this.write("\" />");
}
else {
this.write("<Scalar>");
this.write(output);
this.write("</Scalar>");
}
}
// mark the value complete, let parent container know we need commas
this.completeValue();
return this;
}
@Override
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 name of field");
if (value instanceof Memory)
((Memory)value).copyToStream(this.pw);
else
// TODO handle other object types - reader, etc
this.write(value.toString()); // TODO more efficient
// mark the value complete, let parent container know we need commas
this.completeValue();
return this;
}
private void popState() throws BuilderStateException {
if (this.cstate == null)
throw new BuilderStateException("Cannot pop state when no state is present");
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);
}
private void completeValue() throws BuilderStateException {
// if parent, mark it a having a complete value
if (this.cstate != null) {
this.cstate.ValueComplete = true;
if (this.cstate.State == BuilderState.InField)
this.endField();
else
this.endItem();
}
}
@Override
public Memory toMemory() {
// incompatible concepts
return null;
}
@Override
public CompositeStruct toLocal() {
// incompatible concepts
return null;
}
public void write(String v) {
this.pw.append(v);
}
public void writeChar(char v) {
this.pw.append(v);
}
}