blue.endless.jankson.Jankson Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jankson Show documentation
Show all versions of jankson Show documentation
JSON5 / HJSON parser and preprocessor which preserves ordering and comments
/*
* MIT License
*
* Copyright (c) 2018 Falkreon
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package blue.endless.jankson;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import blue.endless.jankson.impl.Marshaller;
import blue.endless.jankson.impl.ObjectParserContext;
import blue.endless.jankson.impl.ParserContext;
import blue.endless.jankson.impl.SyntaxError;
public class Jankson {
private Deque> contextStack = new ArrayDeque<>();
private JsonObject root;
private int line = 0;
private int column = 0;
private int withheldCodePoint = -1;
private Marshaller marshaller = new Marshaller();
private int retries = 0;
private SyntaxError delayedError = null;
private Jankson(Builder builder) {}
public JsonObject load(String s) throws SyntaxError {
ByteArrayInputStream in = new ByteArrayInputStream(s.getBytes(Charset.forName("UTF-8")));
try {
return load(in);
} catch (IOException ex) {
throw new RuntimeException(ex); //ByteArrayInputStream never throws
}
}
public JsonObject load(File f) throws IOException, SyntaxError {
try(InputStream in = new FileInputStream(f)) {
return load(in);
}
}
private static boolean isLowSurrogate(int i) {
return (i & 0b1100_0000) == 0b1000_0000;
}
private static final int BAD_CHARACTER = 0xFFFD;
public int getCodePoint(InputStream in) throws IOException {
int i = in.read();
if (i==-1) return -1;
if ((i & 0b10000000)==0) return i; // \u0000..\u00FF is easy
if ((i & 0b1111_1000) == 0b1111_0000) { //Character is 4 UTF-8 code points
int codePoint = i & 0b111;
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
return codePoint;
} else if ((i & 0b1111_0000) == 0b1110_0000) { //Character is 4 UTF-8 code points
int codePoint = i & 0b1111;
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
return codePoint;
} else if ((i & 0b1110_0000) == 0b1100_0000) { //Character is 4 UTF-8 code points
int codePoint = i & 0b1111;
i = in.read();
if (i==-1) return -1;
if (!isLowSurrogate(i)) return BAD_CHARACTER;
codePoint <<= 6;
codePoint |= (i & 0b0011_1111);
return codePoint;
}
//we know it's 0b10xx_xxxx down here, so it's an orphaned low surrogate.
return BAD_CHARACTER;
}
public JsonObject load(InputStream in) throws IOException, SyntaxError {
withheldCodePoint = -1;
root = null;
push(new ObjectParserContext(), (it)->{
root = it;
});
//int codePoint = 0;
while (root==null) {
if (delayedError!=null) {
throw delayedError;
}
if (withheldCodePoint!=-1) {
retries++;
if (retries>25) throw new IOException("Parser got stuck near line "+line+" column "+column);
processCodePoint(withheldCodePoint);
} else {
int inByte = getCodePoint(in);
if (inByte==-1) {
//Walk up the stack sending EOF to things until either an error occurs or the stack completes
while(!contextStack.isEmpty()) {
ParserFrame> frame = contextStack.pop();
try {
frame.context.eof();
} catch (SyntaxError error) {
error.setStartParsing(frame.startLine, frame.startCol);
error.setEndParsing(line, column);
throw error;
}
}
if (root==null) root = new JsonObject();
return root;
}
processCodePoint(inByte);
}
}
return root;
}
public T fromJson(JsonObject obj, Class clazz) {
return marshaller.marshall(clazz, obj);
}
public T fromJson(String json, Class clazz) throws SyntaxError {
JsonObject obj = load(json);
return fromJson(obj, clazz);
}
public JsonElement toJson(T t) {
return marshaller.serialize(t);
}
public JsonElement toJson(T t, Marshaller alternateMarshaller) {
return alternateMarshaller.serialize(t);
}
private void processCodePoint(int codePoint) throws SyntaxError {
ParserFrame> frame = contextStack.peek();
if (frame==null) throw new IllegalStateException("Parser problem! The ParserContext stack underflowed! (line "+line+", col "+column+")");
//Do a limited amount of tail call recursion
try {
if (frame.context().isComplete()) {
contextStack.pop();
frame.supply();
frame = contextStack.peek();
}
} catch (SyntaxError error) {
error.setStartParsing(frame.startLine, frame.startCol);
error.setEndParsing(line, column);
throw error;
}
try {
boolean consumed = frame.context.consume(codePoint, this);
if (frame.context.isComplete()) {
contextStack.pop();
frame.supply();
}
if (consumed) {
withheldCodePoint = -1;
retries=0;
} else {
withheldCodePoint = codePoint;
}
} catch (SyntaxError error) {
error.setStartParsing(frame.startLine, frame.startCol);
error.setEndParsing(line, column);
throw error;
}
column++;
if (codePoint=='\n') {
line++;
column = 0;
}
}
/** Pushes a context onto the stack. MAY ONLY BE CALLED BY THE ACTIVE CONTEXT */
public void push(ParserContext t, Consumer consumer) {
ParserFrame frame = new ParserFrame(t, consumer);
frame.startLine = line;
frame.startCol = column;
contextStack.push(frame);
}
public Marshaller getMarshaller() {
return marshaller;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
Marshaller marshaller = new Marshaller();
/**
* Registers a deserializer that can transform a JsonObject into an instance of the specified class. Please note
* that these type adapters are unsuitable for generic types, as these types are erased during jvm execution.
* @param clazz The class to register deserialization for
* @param adapter A function which can produce an object representing the supplied JsonObject
* @return This Builder for further modification.
*/
public Builder registerTypeAdapter(Class clazz, Function adapter) {
marshaller.registerTypeAdapter(clazz, adapter);
return this;
}
/**
* Registers a marshaller for primitive types. Most built-in json and java types are already supported, but this
* allows one to change the deserialization behavior of Json primitives. Please note that these adapters are not
* suitable for generic types, as these types are erased during jvm execution.
* @param clazz
* @param adapter
* @return
*/
public Builder registerPrimitiveTypeAdapter(Class clazz, Function