/*******************************************************************************
* This file is part of RedReader.
*
* RedReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RedReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.quantumbadger.redreader.jsonwrap;
import android.support.annotation.IntDef;
import com.fasterxml.jackson.core.JsonFactory;
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.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
/**
* Contains a literal, object, or array value, and is responsible for parsing
* the incoming JSON stream.
*
* <p>
* <b>To parse a JSON stream, call the JsonValue constructor with a JsonParser
* as the argument, then call the "build" method (with the same JsonParser as an
* argument) in another thread.</b>
* </p>
*/
public final class JsonValue {
public static final int TYPE_OBJECT = 0;
public static final int TYPE_ARRAY = 1;
public static final int TYPE_NULL = 2;
public static final int TYPE_BOOLEAN = 3;
public static final int TYPE_STRING = 4;
public static final int TYPE_FLOAT = 5;
public static final int TYPE_INTEGER = 6;
@IntDef({TYPE_OBJECT, TYPE_ARRAY, TYPE_NULL, TYPE_BOOLEAN, TYPE_STRING, TYPE_FLOAT,
TYPE_INTEGER})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {}
private final @Type int type;
private final Object value;
private JsonParser jp = null;
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param jp
* The incoming JSON stream
* @throws java.io.IOException
*/
public JsonValue(final JsonParser jp) throws IOException {
this(jp, jp.nextToken());
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final InputStream source) throws IOException {
this(new JsonFactory().createParser(source));
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final URL source) throws IOException {
this(new JsonFactory().createParser(source));
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final byte[] source) throws IOException {
this(new JsonFactory().createParser(source));
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final String source) throws IOException {
this(new JsonFactory().createParser(source));
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final File source) throws IOException {
this(new JsonFactory().createParser(source));
}
/**
* Begins parsing a JSON stream into a tree structure. The JsonValue object
* created contains the value at the root of the tree.
*
* This constructor will block until the first JSON token is received. To
* continue building the tree, the "build" method (inherited from
* JsonBuffered) must be called in another thread.
*
* @param source
* The source of incoming JSON data.
* @throws java.io.IOException
*/
public JsonValue(final Reader source) throws IOException {
this(new JsonFactory().createParser(source));
}
// The main constructor
protected JsonValue(final JsonParser jp, final JsonToken firstToken) throws IOException {
switch(firstToken) {
case START_OBJECT:
type = TYPE_OBJECT;
value = new JsonBufferedObject();
this.jp = jp;
break;
case START_ARRAY:
type = TYPE_ARRAY;
value = new JsonBufferedArray();
this.jp = jp;
break;
case VALUE_FALSE:
type = TYPE_BOOLEAN;
value = false;
break;
case VALUE_TRUE:
type = TYPE_BOOLEAN;
value = true;
break;
case VALUE_NULL:
type = TYPE_NULL;
value = null;
break;
case VALUE_STRING:
type = TYPE_STRING;
value = jp.getValueAsString();
break;
case VALUE_NUMBER_FLOAT:
//noinspection FloatingPointEquality,UnnecessaryExplicitNumericCast
if(jp.getValueAsDouble() == (double)jp.getValueAsLong()) {
type = TYPE_INTEGER;
value = jp.getValueAsLong();
} else {
type = TYPE_FLOAT;
value = jp.getValueAsDouble();
}
break;
case VALUE_NUMBER_INT:
type = TYPE_INTEGER;
value = jp.getValueAsLong();
break;
default:
throw new JsonParseException("Expecting an object, literal, or array", jp.getCurrentLocation());
}
}
/**
* Continues the process of parsing the specified JSON stream.
*
* This method will block until the stream is fully parsed, but it is
* possible to make use of this value and its descendants in other threads
* while this is occurring.
*
* @throws java.io.IOException
*/
public void buildInThisThread() throws IOException {
if(type == TYPE_OBJECT || type == TYPE_ARRAY) {
((JsonBuffered)value).build(jp);
}
this.jp = null;
}
/**
* @return The type of value this JsonValue contains.
*/
public @Type int getType() {
return type;
}
/**
* @return True if the type of this value is NULL.
*/
public boolean isNull() {
return type == TYPE_NULL;
}
/**
* @return If this JsonValue contains a JSON object, then this method
* returns that object. The type of value this JsonValue contains
* can be checked with the getType() method.
*/
public JsonBufferedObject asObject() {
switch(type) {
case TYPE_NULL:
return null;
default:
return (JsonBufferedObject)value;
}
}
public <E> E asObject(final Class<E> clazz) throws InstantiationException, IllegalAccessException, InterruptedException, IOException, NoSuchMethodException, InvocationTargetException {
switch(type) {
case TYPE_NULL:
return null;
default:
return asObject().asObject(clazz);
}
}
/**
* @return If this JsonValue contains a JSON array, then this method returns
* that array. The type of value this JsonValue contains can be
* checked with the getType() method.
*/
public JsonBufferedArray asArray() {
switch(type) {
case TYPE_NULL:
return null;
default:
return (JsonBufferedArray)value;
}
}
/**
* @return If this JsonValue contains a boolean, then this method returns
* that boolean. The type of value this JsonValue contains can be
* checked with the getType() method.
*/
public Boolean asBoolean() {
switch(type) {
case TYPE_NULL:
return null;
default:
return (Boolean)value;
}
}
/**
* @return If this JsonValue contains a string, then this method returns
* that string. The type of value this JsonValue contains can be
* checked with the getType() method.
*/
public String asString() {
switch(type) {
case TYPE_FLOAT:
return String.valueOf(asDouble());
case TYPE_INTEGER:
return String.valueOf(asLong());
case TYPE_BOOLEAN:
return String.valueOf(asBoolean());
case TYPE_NULL:
return null;
default:
return (String)value;
}
}
/**
* @return If this JsonValue contains a double, then this method returns
* that double. The type of value this JsonValue contains can be
* checked with the getType() method.
*/
public Double asDouble() {
switch(type) {
case TYPE_NULL:
return null;
case TYPE_INTEGER:
return ((Long)value).doubleValue();
case TYPE_STRING:
return Double.parseDouble(asString());
default:
return (Double)value;
}
}
/**
* @return If this JsonValue contains an integer, then this method returns
* that integer. The type of value this JsonValue contains can be
* checked with the getType() method.
*/
public Long asLong() {
switch(type) {
case TYPE_NULL:
return null;
case TYPE_FLOAT:
return ((Double)value).longValue();
case TYPE_STRING:
return Long.parseLong(asString());
default:
return (Long)value;
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
try {
prettyPrint(0, sb);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
protected void prettyPrint(final int indent, final StringBuilder sb) throws InterruptedException, IOException {
switch(type) {
case TYPE_BOOLEAN: sb.append(asBoolean()); break;
case TYPE_FLOAT: sb.append(asDouble()); break;
case TYPE_INTEGER: sb.append(asLong()); break;
case TYPE_NULL: sb.append("null"); break;
case TYPE_STRING: sb.append("\"").append(asString().replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); break;
case TYPE_ARRAY:
case TYPE_OBJECT: ((JsonBuffered)value).prettyPrint(indent, sb); break;
}
}
public void join() throws InterruptedException {
if(getType() == TYPE_ARRAY || getType() == TYPE_OBJECT) {
((JsonBuffered)value).join();
}
}
}