feat(serialize-xml): initial work on XmlWriter

This commit is contained in:
Johannes Frohnmeyer 2024-04-20 16:43:00 +02:00
parent 8992e42393
commit dc48cc436c
Signed by: Johannes
GPG Key ID: E76429612C2929F4

View File

@ -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";
}
};
}
}