package com.dropbox.core.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
public abstract class JsonExtractor<T>
{
public abstract T extract(JsonParser parser)
throws IOException, JsonExtractionException;
public final T extractField(JsonParser parser, String fieldName, T v)
throws IOException, JsonExtractionException
{
if (v != null) throw new JsonExtractionException("duplicate field \"" + fieldName + "\"", parser.getTokenLocation());
return extract(parser);
}
public final T extractOptional(JsonParser parser)
throws IOException, JsonExtractionException
{
if (parser.getCurrentToken() == JsonToken.VALUE_NULL) {
parser.nextToken();
return null;
} else {
return extract(parser);
}
}
/**
* A wrapper around 'JsonParser.nextToken' that throws our own better {@link JsonExtractionException}
* instead of Jackson's {@link JsonParseException}.
* <p>
* JsonParseException is bad for two reasons. First, it extends IOException, which makes it easy to
* miss. Second, there's no way to get the original error message, which makes it hard to chain
* logical location information (see {@link JsonExtractionException#addFieldContext} and
* {@link JsonExtractionException#addArrayContext}).
*/
public static JsonToken nextToken(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
return parser.nextToken();
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
}
// ------------------------------------------------------------------
// Delimiter checking helpers.
public static JsonLocation expectObjectStart(JsonParser parser)
throws IOException, JsonExtractionException
{
if (parser.getCurrentToken() != JsonToken.START_OBJECT) {
throw new JsonExtractionException("expecting the start of an object (\"{\")", parser.getTokenLocation());
}
JsonLocation loc = parser.getTokenLocation();
nextToken(parser);
return loc;
}
public static void expectObjectEnd(JsonParser parser)
throws IOException, JsonExtractionException
{
if (parser.getCurrentToken() != JsonToken.END_OBJECT) {
throw new JsonExtractionException("expecting the end of an object (\"}\")", parser.getTokenLocation());
}
nextToken(parser);
}
public static JsonLocation expectArrayStart(JsonParser parser)
throws IOException, JsonExtractionException
{
if (parser.getCurrentToken() != JsonToken.START_ARRAY) {
throw new JsonExtractionException("expecting the start of an array (\"[\")", parser.getTokenLocation());
}
JsonLocation loc = parser.getTokenLocation();
nextToken(parser);
return loc;
}
public static boolean isArrayEnd(JsonParser parser)
{
return (parser.getCurrentToken() == JsonToken.END_ARRAY);
}
public static void skipValue(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
parser.skipChildren();
parser.nextToken();
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
}
// ------------------------------------------------------------------
// Helpers for various types.
public static long extractUnsignedLong(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
long v = parser.getLongValue();
if (v < 0) {
throw new JsonExtractionException("expecting a non-negative number, got: " + v, parser.getTokenLocation());
}
parser.nextToken();
return v;
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
}
public static long extractUnsignedLongField(JsonParser parser, String fieldName, long v)
throws IOException, JsonExtractionException
{
if (v >= 0) throw new JsonExtractionException("duplicate field \"" + fieldName + "\"", parser.getCurrentLocation());
return JsonExtractor.extractUnsignedLong(parser);
}
public static final JsonExtractor<String> StringExtractor = new JsonExtractor<String>()
{
public String extract(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
String v = parser.getText();
parser.nextToken();
return v;
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
}
};
public static final JsonExtractor<Boolean> BooleanExtractor = new JsonExtractor<Boolean>()
{
public Boolean extract(JsonParser parser)
throws IOException, JsonExtractionException
{
return extractBoolean(parser);
}
};
public static boolean extractBoolean(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
boolean b = parser.getBooleanValue();
parser.nextToken();
return b;
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
}
/**
* If you're implementing a {@link JsonExtractor} for a JSON object, you can use this to map
* field names to a number you can {@code switch} on to efficiently locate assign a
* field.
*/
public static final class FieldMapping
{
// This is not optimized. Potential optimizations:
// - Store 'int' values instead of 'Integer' values.
// - Don't use "HashMap". Do something gperf-like that generates a faster hash
// function for when you know the valid strings ahead-of-time.
// - The get() could take (char[], offset, length) instead of String, which we can
// provide straight from JsonParser's internal buffer. This makes error reporting
// tricky, though, because we won't have a string for addFieldContext.
public final HashMap<String,Integer> fields;
private FieldMapping(HashMap<String,Integer> fields)
{
assert fields != null;
this.fields = fields;
}
public int get(String fieldName)
{
Integer i = fields.get(fieldName);
if (i == null) return -1;
return i;
}
public static final class Builder
{
private HashMap<String,Integer> fields = new HashMap<String,Integer>();
public void add(String fieldName, int expectedIndex)
{
if (fields == null) throw new IllegalStateException("already called build(); can't call add() anymore");
int i = fields.size();
if (expectedIndex != i) {
throw new IllegalStateException("expectedIndex = " + expectedIndex + ", actual = " + i);
}
Object displaced = fields.put(fieldName, i);
if (displaced != null) {
throw new IllegalStateException("duplicate field name: \"" + fieldName + "\"");
}
}
public FieldMapping build()
{
HashMap<String,Integer> f = fields;
this.fields = null;
return new FieldMapping(f);
}
}
}
private static final JsonFactory jsonFactory = new JsonFactory();
public T extractFullAndClose(InputStream utf8Body)
throws IOException, JsonExtractionException
{
try {
JsonParser parser = jsonFactory.createParser(utf8Body);
return extractFullAndClose(parser);
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
finally {
try { utf8Body.close(); } catch (IOException ex) {
// Can ignore, because we got everything we wanted.
}
}
}
public T extractFull(String body)
throws JsonExtractionException
{
try {
JsonParser parser = jsonFactory.createParser(body);
return extractFullAndClose(parser);
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
catch (IOException ex) {
AssertionError ae = new AssertionError("Got IOException reading from String");
ae.initCause(ae);
throw ae;
}
}
public T extractFull(byte[] utf8Body)
throws JsonExtractionException
{
try {
JsonParser parser = jsonFactory.createParser(utf8Body);
return extractFullAndClose(parser);
}
catch (JsonParseException ex) {
throw JsonExtractionException.fromJackson(ex);
}
catch (IOException ex) {
AssertionError ae = new AssertionError("Got IOException reading from byte[]");
ae.initCause(ae);
throw ae;
}
}
public T extractFromFile(String filePath)
throws FileLoadException
{
return extractFromFile(new File(filePath));
}
public T extractFromFile(File file)
throws FileLoadException
{
try {
InputStream in = new FileInputStream(file);
try {
return extractFullAndClose(in);
}
finally {
try { in.close(); } catch (IOException ex) {
// We already have our data, so ignore.
}
}
}
catch (JsonExtractionException ex) {
throw new FileLoadException.JsonError(file, ex);
}
catch (IOException ex) {
throw new FileLoadException.IOError(file, ex);
}
}
public static abstract class FileLoadException extends Exception
{
protected FileLoadException(String message)
{
super(message);
}
public static final class IOError extends FileLoadException
{
public final IOException reason;
public IOError(File file, IOException reason)
{
super("unable to read file \"" + file.getPath() + "\": " + reason.getMessage());
this.reason = reason;
}
}
public static final class JsonError extends FileLoadException
{
public final JsonExtractionException reason;
public JsonError(File file, JsonExtractionException reason)
{
super(file.getPath() + ": " + reason.getMessage());
this.reason = reason;
}
}
}
public T extractFullAndClose(JsonParser parser)
throws IOException, JsonExtractionException
{
try {
parser.nextToken();
T value = this.extract(parser);
if (parser.getCurrentToken() != null) {
throw new AssertionError("The JSON library should ensure there's no tokens after the main value: "
+ parser.getCurrentToken() + "@" + parser.getCurrentLocation());
}
return value;
}
finally {
if (parser != null) parser.close();
}
}
}