273 lines
8.2 KiB
Java
273 lines
8.2 KiB
Java
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 final 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 XmlWriter(Writer target) {
|
|
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 {
|
|
beforeValue();
|
|
writer.beginTag(nameEncoding.encode(consumeName()));
|
|
push(ARRAY);
|
|
pathIndices[stackSize - 1] = 0;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public XmlWriter endArray() throws IOException {
|
|
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 {
|
|
beforeValue();
|
|
writer.beginTag(nameEncoding.encode(consumeName()));
|
|
push(OBJECT);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public XmlWriter endObject() throws IOException {
|
|
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 {
|
|
writer.comment(comment);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public XmlWriter name(String name) throws IOException {
|
|
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 {
|
|
return literalValue(value);
|
|
}
|
|
|
|
@Override
|
|
public XmlWriter nullValue() throws IOException {
|
|
beforeValue();
|
|
String name = consumeName();
|
|
if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) {
|
|
writer.attribute(nameEncoding.encode(name), null);
|
|
} else {
|
|
writer.beginTag(nameEncoding.encode(name));
|
|
writer.endTag();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public XmlWriter literalValue(String value) throws IOException {
|
|
beforeValue();
|
|
String name = consumeName();
|
|
if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) {
|
|
writer.attribute(name, value);
|
|
} else if (heuristics.shouldUseCData(value, name)) {
|
|
writer.beginTag(nameEncoding.encode(name));
|
|
writer.cdata(value);
|
|
writer.endTag();
|
|
} else {
|
|
writer.beginTag(nameEncoding.encode(name));
|
|
writer.text(value);
|
|
writer.endTag();
|
|
}
|
|
pathIndices[stackSize - 1]++;
|
|
return this;
|
|
}
|
|
|
|
private void beforeValue() throws IOException {
|
|
switch (peek()) {
|
|
case DOCUMENT, ARRAY, OBJECT_VALUE_WRAPPER -> {}
|
|
case OBJECT -> {
|
|
if (deferredName == null) throw new IllegalStateException("Name not set.");
|
|
}
|
|
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 {
|
|
writer.close();
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
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";
|
|
}
|
|
};
|
|
}
|
|
}
|