diff --git a/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/wrapper/XmlWriter.java b/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/wrapper/XmlWriter.java index ed4215b..59efd5f 100644 --- a/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/wrapper/XmlWriter.java +++ b/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/wrapper/XmlWriter.java @@ -1,62 +1,269 @@ package io.gitlab.jfronny.commons.serialize.xml.wrapper; import io.gitlab.jfronny.commons.serialize.SerializeWriter; +import io.gitlab.jfronny.commons.serialize.xml.NativeXmlWriter; +import io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope; +import java.io.Closeable; import java.io.IOException; import java.io.Writer; +import java.util.Arrays; +import java.util.Objects; + +import static io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope.*; + +public class XmlWriter extends SerializeWriter implements Closeable { + private NativeXmlWriter writer; + private int[] stack = new int[32]; + private int stackSize = 0; + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + private String deferredName; + + private Heuristics heuristics = Heuristics.DEFAULT; + private NameEncoding nameEncoding = NameEncoding.DEFAULT; + + { + push(DOCUMENT); + } + + private void push(int newTop) { + if (stackSize == stack.length) { + int newLength = stackSize * 2; + stack = Arrays.copyOf(stack, newLength); + pathIndices = Arrays.copyOf(pathIndices, newLength); + pathNames = Arrays.copyOf(pathNames, newLength); + } + stack[stackSize++] = newTop; + } + + /** Returns the value on the top of the stack. */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** Replace the value on the top of the stack with the given value. */ + private void replaceTop(int topOfStack) { + stack[stackSize - 1] = topOfStack; + } + + public XmlWriter(NativeXmlWriter writer) { + this.writer = Objects.requireNonNull(writer); + } -public class XmlWriter extends SerializeWriter { public XmlWriter(Writer target) { - throw new UnsupportedOperationException("Not yet implemented"); + this(new NativeXmlWriter(target)); + } + + @Override + public XmlWriter setLenient(boolean lenient) { + super.setLenient(lenient); + writer.setLenient(lenient); + return this; + } + + @Override + public boolean isLenient() { + return writer.isLenient(); + } + + public XmlWriter setHeuristics(Heuristics heuristics) { + this.heuristics = Objects.requireNonNull(heuristics); + return this; + } + + public Heuristics getHeuristics() { + return heuristics; + } + + public XmlWriter setNameEncoding(NameEncoding nameEncoding) { + this.nameEncoding = Objects.requireNonNull(nameEncoding); + return this; + } + + public NameEncoding getNameEncoding() { + return nameEncoding; } @Override public XmlWriter beginArray() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + beforeValue(); + writer.beginTag(nameEncoding.encode(consumeName())); + push(ARRAY); + pathIndices[stackSize - 1] = 0; + return this; } @Override public XmlWriter endArray() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + int context = peek(); + if (context != ARRAY) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + if (lenient) nullValue(); + else throw new IllegalStateException("Dangling name: " + deferredName); + } + writer.endTag(); + stackSize--; + pathNames[stackSize] = null; + pathIndices[stackSize - 1]++; + return this; } @Override public XmlWriter beginObject() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + beforeValue(); + writer.beginTag(nameEncoding.encode(consumeName())); + push(OBJECT); + return this; } @Override public XmlWriter endObject() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + int context = peek(); + if (context != OBJECT) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + if (lenient) nullValue(); + else throw new IllegalStateException("Dangling name: " + deferredName); + } + writer.endTag(); + stackSize--; + pathNames[stackSize] = null; + pathIndices[stackSize - 1]++; + return this; } @Override public XmlWriter comment(String comment) throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + writer.comment(comment); + return this; } @Override public XmlWriter name(String name) throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + Objects.requireNonNull(name, "name == null"); + if (deferredName != null) { + throw new IllegalStateException("Already wrote a name, expecting a value."); + } + int context = peek(); + if (context == OBJECT || context == DOCUMENT || context == ARRAY) { + deferredName = name; + return this; + } else throw new IllegalStateException("Name cannot be used in this context."); } @Override public XmlWriter value(String value) throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + return literalValue(value); + } + + @Override + public XmlWriter nullValue() throws IOException { + beforeValue(); + String name = consumeName(); + if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) { + writer.attribute(name, null); + } else { + writer.beginTag(name); + writer.endTag(); + } + return this; } @Override public XmlWriter literalValue(String value) throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + beforeValue(); + String name = consumeName(); + if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) { + writer.attribute(name, value); + } else if (heuristics.shouldUseCData(value, name)) { + writer.beginTag(name); + writer.cdata(value); + writer.endTag(); + } else { + writer.beginTag(name); + writer.text(value); + writer.endTag(); + } + pathIndices[stackSize - 1]++; + return this; + } + + private void beforeValue() throws IOException { + switch (peek()) { + case DOCUMENT, ARRAY, OBJECT_VALUE_WRAPPER -> {} + default -> throw new IllegalStateException("Nesting problem."); + } + } + + private String consumeName() throws IOException { + if (deferredName != null) { + String result = deferredName; + deferredName = null; + pathNames[stackSize - 1] = result; + return result; + } + StringBuilder result = new StringBuilder().append('$'); + for (int i = 0; i < stackSize; i++) { + int scope = stack[i]; + switch (scope) { + case WrapperScope.ARRAY -> { + int pathIndex = pathIndices[i]; + // If index is last path element it points to next array element; have to decrement + result.append('[').append(pathIndex).append(']'); + } + case WrapperScope.OBJECT -> { + result.append('.'); + if (pathNames[i] != null) { + result.append(pathNames[i]); + } + } + case WrapperScope.DOCUMENT -> {} + default -> throw new AssertionError("Unknown scope value: " + scope); + } + } + String guess = heuristics.guessArrayElementName(result.toString()); + pathNames[stackSize - 1] = guess; + return guess; } @Override public void flush() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + writer.close(); } @Override public void close() throws IOException { - throw new UnsupportedOperationException("Not yet implemented"); + writer.close(); + } + + public interface Heuristics { + boolean shouldUseAttribute(String path); + boolean shouldUseCData(String value, String path); + String guessArrayElementName(String path); + + Heuristics DEFAULT = new Heuristics() { + @Override + public boolean shouldUseAttribute(String path) { + return false; + } + + @Override + public boolean shouldUseCData(String value, String path) { + return false; + } + + @Override + public String guessArrayElementName(String path) { + return "item"; + } + }; } }