/* Copyright 2014 hbz, Pascal Christoph.
* Licensed under the Eclipse Public License 1.0 */
package org.lobid.lodmill;
import java.io.IOException;
import java.io.Reader;
import org.culturegraph.mf.framework.DefaultObjectPipe;
import org.culturegraph.mf.framework.StreamReceiver;
import org.culturegraph.mf.framework.annotations.Description;
import org.culturegraph.mf.framework.annotations.In;
import org.culturegraph.mf.framework.annotations.Out;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 com.google.common.io.CharStreams;
/**
* Ejects just plain key-value structures, not paths. Decodes JSON and JSONP.
* The JSON (or JSONP) record may consist of one or n records. Ejects just plain
* key-value structures, not paths! Values of objects in arrays can be handled
* as values clinging to these objects or as part of the root object.
*
* Note: Ejects just plain key-value structures, not paths.
*
* @author Pascal Christoph (dr0i)
*
*/
@Description("Decodes a json(p) record as literals (as key-value pairs).")
@In(Reader.class)
@Out(StreamReceiver.class)
public final class JsonDecoder
extends DefaultObjectPipe<Reader, StreamReceiver> {
private static final String JSON_START_CHAR = "{";
private static final String JSON_CALLBACK = "json_callback";
private JsonParser jsonParser;
private static final Logger LOG = LoggerFactory.getLogger(JsonDecoder.class);
private boolean STARTED;
private boolean JSONP;
private boolean oneRecord = true;
/**
* Sets the whole json structure to be handled as one record.
*
* @param oneRecord if set to true the whole json structure is handled as one
* record, not necessarily as records within records.
*/
public void setOneRecord(final String oneRecord) {
this.oneRecord = true;
}
private void handleValue(final JsonToken currentToken, final String key)
throws IOException, JsonParseException {
{
if (JsonToken.VALUE_STRING == currentToken
|| JsonToken.VALUE_NUMBER_INT == currentToken
|| JsonToken.VALUE_NUMBER_FLOAT == currentToken) {
final String value = this.jsonParser.getText();
LOG.debug("key=" + key + " value=" + value);
getReceiver().literal(key, value);
}
}
}
@Override
public void process(final Reader reader) {
STARTED = false;
LOG.debug("############################ New");
try {
JsonToken currentToken = parseJson(reader);
if (currentToken == null)
return;
processTokens(currentToken);
} catch (final IOException e) {
try {
LOG.warn(e.getLocalizedMessage() + "while computing "
+ this.jsonParser.getText());
} catch (JsonParseException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
private JsonToken parseJson(final Reader reader)
throws IOException, JsonParseException {
String text = CharStreams.toString(reader);
this.jsonParser = new JsonFactory().createParser(text);
JsonToken currentToken = null;
try {
currentToken = this.jsonParser.nextToken();
} catch (final JsonParseException e) {
// is it JSONP ?
if (text.indexOf(JsonDecoder.JSON_START_CHAR) == -1) {
LOG.info("No JSON(P) - ignoring");
return null;
}
currentToken = handleJsonp(text);
}
while (JsonToken.START_OBJECT != currentToken) {
this.jsonParser.nextToken();
}
return currentToken;
}
private void processTokens(JsonToken token)
throws IOException, JsonParseException {
JsonToken currentToken = token;
while (currentToken != null) {
if (JsonToken.START_OBJECT == currentToken) {
if (!STARTED) {
getReceiver().startRecord("");
STARTED = true;
}
currentToken = processRecordContent(this.jsonParser.nextToken());
}
LOG.debug("############################ End");
if (STARTED & !oneRecord) {
getReceiver().endRecord();
STARTED = false;
}
}
}
private JsonToken processRecordContent(JsonToken token)
throws IOException, JsonParseException {
JsonToken currentToken = token;
String key = null;
while (currentToken != null) {
if (JsonToken.FIELD_NAME == currentToken)
key = this.jsonParser.getCurrentName();
if (JsonToken.START_ARRAY == currentToken) {
currentToken = this.jsonParser.nextToken();
if (this.JSONP)
currentToken = this.jsonParser.nextToken();
else {
// break to treat objects in arrays as new objects
if (JsonToken.START_OBJECT == currentToken)
break;
currentToken = handleValuesOfArrays(currentToken, key);
}
}
if (JsonToken.START_OBJECT == currentToken) {
if (this.jsonParser.getCurrentName() == null)
break;
} else
handleValue(currentToken, key);
try {
currentToken = this.jsonParser.nextToken();
} catch (JsonParseException e) {
LOG.debug("Exception at the end of non JSON object, might be JSONP", e);
currentToken = null;
break;
}
}
return currentToken;
}
private JsonToken handleValuesOfArrays(final JsonToken currentToken,
final String key) throws JsonParseException, IOException {
int i = 0;
JsonToken jtoken = currentToken;
while (JsonToken.END_ARRAY != jtoken) {
final String value = this.jsonParser.getText();
LOG.debug("key=" + key + i + " valueArray=" + value);
getReceiver().literal(key + i, value);
jtoken = this.jsonParser.nextToken();
i++;
}
return jtoken;
}
private JsonToken handleJsonp(final String jsonp)
throws IOException, JsonParseException {
JsonToken currentToken;
final String callbackString =
jsonp.substring(0, jsonp.indexOf(JsonDecoder.JSON_START_CHAR) - 1);
final String json = jsonp.substring(
jsonp.indexOf(JsonDecoder.JSON_START_CHAR), jsonp.length() - 1);
this.jsonParser = new JsonFactory().createParser(json);
LOG.debug("key=" + JsonDecoder.JSON_CALLBACK + " value=" + callbackString);
getReceiver().startRecord("");
STARTED = true;
JSONP = true;
getReceiver().literal(JsonDecoder.JSON_CALLBACK, callbackString);
LOG.debug("Json=" + json);
currentToken = this.jsonParser.nextToken();
return currentToken;
}
}