feat(serialize-xml): initial work on XmlWriter
This commit is contained in:
parent
8992e42393
commit
dc48cc436c
@ -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<IOException, XmlWriter> 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<IOException, XmlWriter> {
|
||||
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";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user