package cn.citycraft.GsonAgent.nms.stream; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Field; import cn.citycraft.GsonAgent.api.utils.Utils; import net.minecraft.util.com.google.gson.stream.JsonWriter; public class JsonWriterHandle extends JsonWriter implements cn.citycraft.GsonAgent.api.stream.JsonWriter { /* * From RFC 4627, "All Unicode characters may be placed within the * quotation marks except for the characters that must be escaped: * quotation mark, reverse solidus, and the control characters * (U+0000 through U+001F)." * * We also escape '\u2028' and '\u2029', which JavaScript interprets as * newline characters. This prevents eval() from failing with a syntax * error. http://code.google.com/p/google-gson/issues/detail?id=341 */ private static Field stackField, stackSizeField, indentField, separatorField; static { final Class clzz = JsonWriter.class; try { stackField = Utils.getDeclareField(clzz, "stack"); stackSizeField = Utils.getDeclareField(clzz, "stackSize"); indentField = Utils.getDeclareField(clzz, "indent"); separatorField = Utils.getDeclareField(clzz, "separator"); } catch (final Exception e) { e.printStackTrace(); } } /** * The output data, containing at most one top-level array or object. */ private final Writer out; private String deferredName; private boolean withoutQuotes = false; /** * Creates a new instance that writes a JSON-encoded stream to {@code out}. * For best performance, ensure {@link Writer} is buffered; wrapping in * {@link java.io.BufferedWriter BufferedWriter} if necessary. * * @param out */ public JsonWriterHandle(final Writer out) { super(out); this.out = out; } /** * Begins encoding a new array. Each call to this method must be paired with * a call to {@link #endArray}. * * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle beginArray() throws IOException { writeDeferredName(); return open(EMPTY_ARRAY, "["); } /** * Begins encoding a new object. Each call to this method must be paired * with a call to {@link #endObject}. * * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle beginObject() throws IOException { writeDeferredName(); return open(EMPTY_OBJECT, "{"); } /** * Flushes and closes this writer and the underlying {@link Writer}. * * @throws IOException * if the JSON document is incomplete. */ @Override public void close() throws IOException { out.close(); final int size = this.getStackSize(); if (size > 1 || size == 1 && this.getStack()[size - 1] != NONEMPTY_DOCUMENT) { throw new IOException("Incomplete document"); } this.setStackSize(0); } /** * Ends encoding the current array. * * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle endArray() throws IOException { return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]"); } /** * Ends encoding the current object. * * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle endObject() throws IOException { return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}"); } /** * Ensures all buffered data is written to the underlying {@link Writer} and * flushes that writer. * * @throws java.io.IOException */ @Override public void flush() throws IOException { if (this.getStackSize() == 0) { throw new IllegalStateException("JsonWriterHandle is closed."); } out.flush(); } public JsonWriterHandle getHandle() { return this; } public String getIndent() { return Utils.invokeStringField(this, indentField); } public String getSeparator() { return Utils.invokeStringField(this, separatorField); } public int[] getStack() { return Utils.invokeIntArrayField(this, stackField); } public int getStackSize() { return Utils.invokeIntField(this, stackSizeField); } /** * Encodes the property name. * * @param name * the name of the forthcoming value. May not be null. * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle name(final String name) throws IOException { if (name == null) { throw new NullPointerException("name == null"); } if (deferredName != null) { throw new IllegalStateException(); } if (this.getStackSize() == 0) { throw new IllegalStateException("JsonWriterHandle is closed."); } deferredName = name; withoutQuotes = false; return this; } @Override public JsonWriterHandle nameWithoutQuotes(final String name) throws IOException { this.name(name); withoutQuotes = true; return this; } /** * Encodes {@code null}. * * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle nullValue() throws IOException { if (deferredName != null) { if (this.getSerializeNulls()) { writeDeferredName(); } else { deferredName = null; return this; // skip the name and the value } } beforeValue(false); out.write("null"); return this; } public void setSeparator(final String separator) { Utils.modifyField(this, separatorField, separator); } public void setStack(final int index, final int value) { final int[] newStack = this.getStack(); newStack[index] = value; this.setStack(newStack); } public void setStack(final int[] stack) { Utils.modifyField(this, stackField, stack); } public void setStackSize(final int stackSize) { Utils.modifyField(this, stackSizeField, stackSize); } @Override public void stringExtend(final String value) throws IOException { final String[] replacements = this.isHtmlSafe() ? Utils.HTML_SAFE_REPLACEMENT_CHARS : Utils.REPLACEMENT_CHARS; if (!this.withoutQuotes) { this.out.write("\""); } int last = 0; final int length = value.length(); for (int i = 0; i < length; i++) { final char c = value.charAt(i); String replacement; if (c < 128) { replacement = replacements[c]; if (replacement == null) { continue; } } else if (c == '\u2028') { replacement = "\\u2028"; } else if (c == '\u2029') { replacement = "\\u2029"; } else { continue; } if (last < i) { out.write(value, last, i - last); } out.write(replacement); last = i + 1; } if (last < length) { out.write(value, last, length - last); } if (!this.withoutQuotes) { this.out.write("\""); } withoutQuotes = false; } /** * Encodes {@code value}. * * @param value * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle value(final boolean value) throws IOException { writeDeferredName(); beforeValue(false); out.write(value ? "true" : "false"); return this; } /** * Encodes {@code value}. * * @param value * a finite value. May not be {@link Double#isNaN() NaNs} or * {@link Double#isInfinite() infinities}. * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle value(final double value) throws IOException { if (Double.isNaN(value) || Double.isInfinite(value)) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } writeDeferredName(); beforeValue(false); out.append(Double.toString(value)); return this; } /** * Encodes {@code value}. * * @param value * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle value(final long value) throws IOException { writeDeferredName(); beforeValue(false); out.write(Long.toString(value)); return this; } /** * Encodes {@code value}. * * @param value * a finite value. May not be {@link Double#isNaN() NaNs} or * {@link Double#isInfinite() infinities}. * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle value(final Number value) throws IOException { if (value == null) { return nullValue(); } writeDeferredName(); final String string = value.toString(); if (!this.isLenient() && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } beforeValue(false); out.append(string); return this; } /** * Encodes {@code value}. * * @param value * the literal string value, or null to encode a null literal. * @return this writer. * @throws java.io.IOException */ @Override public JsonWriterHandle value(final String value) throws IOException { if (value == null) { return nullValue(); } writeDeferredName(); beforeValue(false); stringExtend(value); return this; } /** * Inserts any necessary separators and whitespace before a name. Also * adjusts the stack to expect the name's value. */ private void beforeName() throws IOException { final int context = peek(); if (context == NONEMPTY_OBJECT) { // first in object out.write(','); } else if (context != EMPTY_OBJECT) { // not in an object! throw new IllegalStateException("Nesting problem."); } newline(); replaceTop(DANGLING_NAME); } /** * Inserts any necessary separators and whitespace before a literal value, * inline array, or inline object. Also adjusts the stack to expect either a * closing bracket or another element. * * @param root * true if the value is a new array or object, the two values * permitted as top-level elements. */ @SuppressWarnings("fallthrough") private void beforeValue(final boolean root) throws IOException { switch (peek()) { case NONEMPTY_DOCUMENT: if (!this.isLenient()) { throw new IllegalStateException("JSON must have only one top-level value."); } // fall-through case EMPTY_DOCUMENT: // first in document if (!this.isLenient() && !root) { throw new IllegalStateException("JSON must start with an array or an object."); } replaceTop(NONEMPTY_DOCUMENT); break; case EMPTY_ARRAY: // first in array replaceTop(NONEMPTY_ARRAY); newline(); break; case NONEMPTY_ARRAY: // another in array out.append(','); newline(); break; case DANGLING_NAME: // value for name out.append(this.getSeparator()); replaceTop(NONEMPTY_OBJECT); break; default: throw new IllegalStateException("Nesting problem."); } } /** * Closes the current scope by appending any necessary whitespace and the * given bracket. */ private JsonWriterHandle close(final int empty, final int nonempty, final String closeBracket) throws IOException { final int context = peek(); if (context != nonempty && context != empty) { throw new IllegalStateException("Nesting problem."); } if (deferredName != null) { throw new IllegalStateException("Dangling name: " + deferredName); } this.setStackSize(this.getStackSize() - 1); if (context == nonempty) { newline(); } out.write(closeBracket); return this; } private void newline() throws IOException { if (this.getIndent() == null) { return; } out.write("\n"); for (int i = 1, size = this.getStackSize(); i < size; i++) { out.write(this.getIndent()); } } /** * Enters a new scope by appending any necessary whitespace and the given * bracket. */ private JsonWriterHandle open(final int empty, final String openBracket) throws IOException { beforeValue(true); push(empty); out.write(openBracket); return this; } /** * Returns the value on the top of the stack. */ private int peek() { if (this.getStackSize() == 0) { throw new IllegalStateException("JsonWriterHandle is closed."); } return this.getStack()[this.getStackSize() - 1]; } private void push(final int newTop) { int[] stacks = this.getStack(); if (this.getStackSize() == stacks.length) { final int[] newStack = new int[stacks.length * 2]; System.arraycopy(stacks, 0, newStack, 0, stacks.length); stacks = newStack; this.setStack(stacks); } this.setStack(this.getStackSize(), newTop); this.setStackSize(this.getStackSize() + 1); } /** * Replace the value on the top of the stack with the given value. */ private void replaceTop(final int topOfStack) { this.getStack()[this.getStackSize() - 1] = topOfStack; } private void writeDeferredName() throws IOException { if (deferredName != null) { beforeName(); stringExtend(deferredName); deferredName = null; } } }