blob: 122e94f82b61f15df3cbdb1bcab7318474841a75 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.pdx;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonParser.NumberType;
import com.fasterxml.jackson.core.JsonToken;
import org.apache.geode.cache.RegionService;
import org.apache.geode.cache.client.internal.ProxyCache;
import org.apache.geode.cache.client.internal.UserAttributes;
import org.apache.geode.pdx.internal.json.JSONToPdxMapper;
import org.apache.geode.pdx.internal.json.PdxInstanceHelper;
import org.apache.geode.pdx.internal.json.PdxInstanceSortedHelper;
import org.apache.geode.pdx.internal.json.PdxListHelper;
import org.apache.geode.pdx.internal.json.PdxToJSON;
import org.apache.geode.util.internal.GeodeGlossary;
/**
* <p>
* JSONFormatter has a static method {@link JSONFormatter#fromJSON(String, String...)} to convert a
* JSON document into a {@link PdxInstance} and a static method
* {@link JSONFormatter#toJSON(PdxInstance)} to convert a {@link PdxInstance} into a JSON Document.
* </p>
* <p>
* Using these methods an applications may convert a JSON document into a PdxInstance for storing in
* the cache. Indexes can then be defined on the PdxInstances so that queries can be performed using
* OQL. Queries will return PdxInstances and these can be turned back back into JSON documents using
* JSONFormatter.
* </p>
* <p>
* JSONFormatter treats values in a json document as number(byte, short, int, long..), string,
* array, object, 'true', 'false' or 'null'. These correspond to the following java types:
* </p>
*
* <table>
* <th>JSON</th>
* <th>Java</th>
* <tr>
* <td>object</td>
* <td>{@link PdxInstance}</td>
* </tr>
* <tr>
* <td>arrays</td>
* <td>{@link java.util.LinkedList}</td>
* </tr>
* <tr>
* <td>BigDecimal</td>
* <td>{@link BigDecimal}</td>
* </tr>
* <tr>
* <td>BigInterger</td>
* <td>{@link BigInteger}</td>
* </tr>
* <tr>
* <td>Double</td>
* <td>double</td>
* </tr>
* <tr>
* <td>float</td>
* <td>float</td>
* </tr>
* <tr>
* <td>boolean</td>
* <td>boolean</td>
* </tr>
* <tr>
* <td>Integer</td>
* <td>byte, short or int</td>
* </tr>
* <tr>
* <td>null</td>
* <td>null</td>
* </tr>
* </table>
*/
public class JSONFormatter {
public static final String JSON_CLASSNAME = "__GEMFIRE_JSON";
/***
* By setting "gemfire.sort-json-field-names" to true, enables serialization of JSON field in JSON
* document to be sorted. That can help to reduce the number of pdx typeId generation if different
* JSON documents have same fields in the different order.
*/
public static final String SORT_JSON_FIELD_NAMES_PROPERTY =
GeodeGlossary.GEMFIRE_PREFIX + "pdx.mapper.sort-json-field-names";
enum states {
NONE, OBJECT_START, FIELD_NAME, SCALAR_FOUND, LIST_FOUND, LIST_ENDS, OBJECT_ENDS
}
private RegionService regionService;
public JSONFormatter() {}
public JSONFormatter(RegionService regionService) {
this.regionService = regionService;
}
public RegionService getRegionService() {
return regionService;
}
/**
* Converts a String JSON document into a PdxInstance
*
* @param jsonString The JSON String to convert to PDX
* @throws JSONFormatterException if unable to create the PdxInstance
* @return The PdxInstance
*/
public static PdxInstance fromJSON(String jsonString) {
return new JSONFormatter().toPdxInstance(jsonString);
}
/**
* Converts a String JSON document into a PdxInstance
*
* @param jsonString The JSON String to convert to PDX
* @param identityFields Any desired identity fields on the JSON object to be used for equals and
* hashCode computations
* @throws JSONFormatterException if unable to create the PdxInstance
* @return The PdxInstance
*/
public static PdxInstance fromJSON(String jsonString, String... identityFields) {
return new JSONFormatter().toPdxInstance(jsonString, identityFields);
}
/**
* Converts a Byte Array JSON document into a PdxInstance
*
* @param jsonByteArray The JSON Object as a byte array to convert to PDX
* @throws JSONFormatterException if unable to create the PdxInstance
* @return The PdxInstance
*/
public static PdxInstance fromJSON(byte[] jsonByteArray) {
return new JSONFormatter().toPdxInstance(jsonByteArray);
}
/**
* Converts a Byte Array JSON document into a PdxInstance
*
* @param jsonByteArray The JSON Object as a byte array to convert to PDX
* @param identityFields Any desired identity fields on the JSON object to be used for equals and
* hashCode computations
* @throws JSONFormatterException if unable to create the PdxInstance
* @return The PdxInstance
*/
public static PdxInstance fromJSON(byte[] jsonByteArray, String... identityFields) {
return new JSONFormatter().toPdxInstance(jsonByteArray, identityFields);
}
/**
* Converts a JSON document (String or Byte Array) into a PdxInstance
*
* @param json The JSON document (String or Byte Array) to convert to PDX
* @param identityFields Any desired identity fields on the JSON object to be used for equals and
* hashCode computations
* @return The PdxInstance
* @throws JSONFormatterException if unable to create the PdxInstance
*/
public PdxInstance toPdxInstance(Object json, String... identityFields) {
if (regionService != null && regionService instanceof ProxyCache) {
ProxyCache proxyCache = (ProxyCache) regionService;
UserAttributes.userAttributes.set(proxyCache.getUserAttributes());
}
JsonParser jp = null;
try {
if (json instanceof String) {
jp = new JsonFactory().createParser((String) json);
} else if (json instanceof byte[]) {
jp = new JsonFactory().createParser((byte[]) json);
} else {
throw new JSONFormatterException("Could not parse the " + json.getClass() + " type");
}
enableJSONParserFeature(jp);
return getPdxInstance(jp, states.NONE, null, identityFields).getPdxInstance();
} catch (JsonParseException jpe) {
throw new JSONFormatterException("Could not parse JSON document ", jpe);
} catch (IOException e) {
throw new JSONFormatterException("Could not parse JSON document: " + jp.getCurrentLocation(),
e);
} catch (Exception e) {
throw new JSONFormatterException("Could not parse JSON document: " + jp.getCurrentLocation(),
e);
} finally {
UserAttributes.userAttributes.set(null);
}
}
private void enableJSONParserFeature(JsonParser jp) {
jp.enable(Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
jp.enable(Feature.ALLOW_UNQUOTED_FIELD_NAMES);
}
/**
* Converts a PdxInstance into a JSON document in String form
*
* @param pdxInstance the JSON string.
* @return the JSON string
* JSONFormatterException if unable to create the JSON document
*/
public static String toJSON(PdxInstance pdxInstance) {
return new JSONFormatter().fromPdxInstance(pdxInstance);
}
/**
* Converts a PdxInstance into a JSON document
*
* @param pdxInstance The PdxInstance to convert
* @return the JSON string
* @throws JSONFormatterException if unable to create the JSON document
*/
public String fromPdxInstance(PdxInstance pdxInstance) {
try {
PdxToJSON pj = new PdxToJSON(pdxInstance);
return pj.getJSON();
} catch (Exception e) {
throw new JSONFormatterException("Could not create JSON document from PdxInstance", e);
}
}
/**
* Converts a PdxInstance into a JSON document in byte-array form
*
* @param pdxInstance The PdxInstance to convert
* @return the JSON byte array
* @throws JSONFormatterException if unable to create the JSON document
*/
public static byte[] toJSONByteArray(PdxInstance pdxInstance) {
return new JSONFormatter().toJsonByteArrayFromPdxInstance(pdxInstance);
}
/**
* Converts a PdxInstance into a JSON document in byte-array form
*
* @param pdxInstance The PdxInstance to convert
* @return the JSON byte array
* @throws JSONFormatterException if unable to create the JSON document
*/
public byte[] toJsonByteArrayFromPdxInstance(PdxInstance pdxInstance) {
try {
PdxToJSON pj = new PdxToJSON(pdxInstance);
return pj.getJSONByteArray();
} catch (Exception e) {
throw new JSONFormatterException("Could not create JSON document from PdxInstance", e);
}
}
private JSONToPdxMapper createJSONToPdxMapper(String className, JSONToPdxMapper parent,
String... identityFields) {
if (Boolean.getBoolean(SORT_JSON_FIELD_NAMES_PROPERTY)) {
return new PdxInstanceSortedHelper(className, parent, identityFields);
} else {
return new PdxInstanceHelper(className, parent, identityFields);
}
}
private JSONToPdxMapper getPdxInstance(JsonParser jp, states currentState,
JSONToPdxMapper currentPdxInstance, String... identityFields) throws IOException {
String currentFieldName = null;
if (currentState == states.OBJECT_START && currentPdxInstance == null) {
currentPdxInstance = createJSONToPdxMapper(null, null, identityFields);// from getlist
}
while (true) {
JsonToken nt = jp.nextToken();
if (nt == null) {
return currentPdxInstance;
}
switch (nt) {
case START_OBJECT: {
objectStarts(currentState);
currentState = states.OBJECT_START;
// need to create new PdxInstance
// root object will not name, so create classname lazily from all members.
// child object will have name; but create this as well lazily from all members
JSONToPdxMapper tmp =
createJSONToPdxMapper(currentFieldName, currentPdxInstance, identityFields);
currentPdxInstance = tmp;
break;
}
case END_OBJECT: {
// pdxinstnce ends
objectEnds(currentState);
currentState = states.OBJECT_ENDS;
currentPdxInstance.endObjectField("endobject");
if (currentPdxInstance.getParent() == null) {
return currentPdxInstance;// inner pdxinstance in list
}
JSONToPdxMapper tmp = currentPdxInstance;
currentPdxInstance = currentPdxInstance.getParent();
currentPdxInstance.addObjectField(tmp.getPdxFieldName(), tmp.getPdxInstance());
break;
}
case FIELD_NAME: {
fieldFound(currentState);
// field name(object name, value may be object, string, array number etc)
if (currentState == states.OBJECT_START) {
currentPdxInstance.setPdxFieldName(currentFieldName);
}
currentFieldName = jp.getText();// not a object name
currentState = states.FIELD_NAME;
break;
}
case NOT_AVAILABLE: {
throw new IllegalStateException("NOT_AVAILABLE token found");
// break;
}
case START_ARRAY: {
// need to create array; fieldname may be there; will it case it not there
arrayStarts(currentState);
currentState = states.LIST_FOUND;
PdxListHelper list = getList(jp, currentState, null);
currentPdxInstance.addListField(currentFieldName, list);
currentState = states.LIST_ENDS;
currentFieldName = null;
break;
}
case END_ARRAY: {
// array is end
throw new IllegalStateException(
"END_ARRAY token found in getPdxInstance while current state is " + currentState);
}
case VALUE_EMBEDDED_OBJECT: {
throw new IllegalStateException(
"VALUE_EMBEDDED_OBJECT token found in getPdxInstance while current state is "
+ currentState);
}
case VALUE_FALSE: {
// write boolen
boolFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxInstance.addBooleanField(currentFieldName, jp.getValueAsBoolean());
currentFieldName = null;
break;
}
case VALUE_NULL: {
// write null
nullFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxInstance.addNullField(currentFieldName);
currentFieldName = null;
break;
}
case VALUE_NUMBER_FLOAT: {
// write double/float
doubleFound(currentState);
currentState = states.SCALAR_FOUND;
// currentPdxInstance.addDoubleField(currentFieldName, jp.getDoubleValue());
setNumberField(jp, currentPdxInstance, currentFieldName);
currentFieldName = null;
break;
}
case VALUE_NUMBER_INT: {
// write int
intFound(currentState);
currentState = states.SCALAR_FOUND;
// currentPdxInstance.addIntField(currentFieldName, jp.getIntValue());
setNumberField(jp, currentPdxInstance, currentFieldName);
currentFieldName = null;
break;
}
case VALUE_STRING: {
// write string
stringFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxInstance.addObjectField(currentFieldName, new String(jp.getText()));
currentFieldName = null;
break;
}
case VALUE_TRUE: {
// write bool
boolFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxInstance.addBooleanField(currentFieldName, jp.getValueAsBoolean());
currentFieldName = null;
break;
}
default: {
throw new IllegalStateException("Token not handled " + nt);
}
}
}
}
private void setNumberField(JsonParser jp, JSONToPdxMapper pih, String fieldName)
throws IOException {
NumberType nt = jp.getNumberType();
switch (nt) {
case BIG_DECIMAL:
pih.addBigDecimalField(fieldName, jp.getDecimalValue());
break;
case BIG_INTEGER: {
BigInteger bi = jp.getBigIntegerValue();
pih.addBigIntegerField(fieldName, bi);
}
break;
case DOUBLE:
pih.addDoubleField(fieldName, jp.getDoubleValue());
break;
case FLOAT:
pih.addFloatField(fieldName, jp.getFloatValue());
break;
case INT: {
int val = jp.getIntValue();
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
pih.addIntField(fieldName, val);
} else if (val > Byte.MAX_VALUE || val < Byte.MIN_VALUE) {
pih.addShortField(fieldName, (short) val);
} else {
pih.addByteField(fieldName, (byte) val);
}
}
break;
case LONG:
pih.addLongField(fieldName, jp.getLongValue());
break;
default:
throw new IllegalStateException("setNumberField:unknow number type " + nt);
}
}
private void setNumberField(JsonParser jp, PdxListHelper pih) throws IOException {
NumberType nt = jp.getNumberType();
switch (nt) {
case BIG_DECIMAL:
pih.addBigDecimalField(jp.getDecimalValue());
break;
case BIG_INTEGER: {
BigInteger bi = jp.getBigIntegerValue();
pih.addBigIntegerField(bi);
}
break;
case DOUBLE:
pih.addDoubleField(jp.getDoubleValue());
break;
case FLOAT:
pih.addFloatField(jp.getFloatValue());
break;
case INT: {
int val = jp.getIntValue();
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
pih.addIntField(val);
} else if (val > Byte.MAX_VALUE || val < Byte.MIN_VALUE) {
pih.addShortField((short) val);
} else {
pih.addByteField((byte) val);
}
}
break;
case LONG:
pih.addLongField(jp.getLongValue());
break;
default:
throw new IllegalStateException("setNumberField:unknow number type " + nt);
}
}
private PdxListHelper getList(JsonParser jp, states currentState, PdxListHelper currentPdxList)
throws JsonParseException, IOException {
String currentFieldName = null;
currentPdxList = new PdxListHelper(currentPdxList, null);
while (true) {
JsonToken nt = jp.nextToken();
if (nt == null) {
return currentPdxList;
}
switch (nt) {
case START_OBJECT: {
objectStarts(currentState);
currentState = states.OBJECT_START;
// need to create new PdxInstance
// root object will not name, so create classname lazily from all members.
// child object will have name; but create this as well lazily from all members
JSONToPdxMapper tmp = getPdxInstance(jp, currentState, null);
currentPdxList.addObjectField(currentFieldName, tmp);
currentState = states.OBJECT_ENDS;
break;
}
case END_OBJECT: {
// pdxinstnce ends
throw new IllegalStateException(
"getList got token END_OBJECT while current state is " + currentState);
}
case FIELD_NAME: {
throw new IllegalStateException(
"getList got token FIELD_NAME while current state is " + currentState);
}
case NOT_AVAILABLE: {
throw new IllegalStateException(
"NOT_AVAILABLE token found in getList current state is " + currentState);
// break;
}
case START_ARRAY: {
// need to create array; fieldname may be there; will it case it not there
arrayStarts(currentState);
PdxListHelper tmp = currentPdxList.addListField();
currentPdxList = tmp;
currentState = states.LIST_FOUND;
break;
}
case END_ARRAY: {
// array is end
arrayEnds(currentState);
currentState = states.LIST_ENDS;
if (currentPdxList.getParent() == null) {
return currentPdxList;
}
currentPdxList = currentPdxList.getParent();
break;
}
case VALUE_EMBEDDED_OBJECT: {
throw new IllegalStateException("VALUE_EMBEDDED_OBJECT token found");
}
case VALUE_FALSE: {
// write boolen
boolFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxList.addBooleanField(jp.getBooleanValue());
break;
}
case VALUE_NULL: {
// write null
nullFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxList.addNullField(null);
break;
}
case VALUE_NUMBER_FLOAT: {
// write double/float
doubleFound(currentState);
currentState = states.SCALAR_FOUND;
// currentPdxList.addDoubleField(jp.getDoubleValue());
setNumberField(jp, currentPdxList);
break;
}
case VALUE_NUMBER_INT: {
// write int
intFound(currentState);
currentState = states.SCALAR_FOUND;
// currentPdxList.addIntField(jp.getIntValue());
setNumberField(jp, currentPdxList);
break;
}
case VALUE_STRING: {
// write string
stringFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxList.addStringField(jp.getText());
currentFieldName = null;
break;
}
case VALUE_TRUE: {
// write bool
boolFound(currentState);
currentState = states.SCALAR_FOUND;
currentPdxList.addBooleanField(jp.getBooleanValue());
break;
}
default: {
throw new IllegalStateException("Token not handled in getlist" + nt);
}
}
}
}
private boolean objectStarts(states currentState) {
switch (currentState) {
case NONE:
case FIELD_NAME:
case OBJECT_ENDS:// in list
case SCALAR_FOUND:// inlist
case LIST_FOUND:
case LIST_ENDS:
return true;
default:
throw new IllegalStateException("Object start called when state is " + currentState);
}
}
private boolean objectEnds(states currentState) {
switch (currentState) {
case OBJECT_START: // when empty object on field
case SCALAR_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:// inner object closes
return true;
default:
throw new IllegalStateException("Object ends called when state is " + currentState);
}
}
private boolean arrayStarts(states currentState) {
switch (currentState) {
case SCALAR_FOUND:
case FIELD_NAME:
case LIST_FOUND:
case LIST_ENDS:
return true;
default:
throw new IllegalStateException("Array start called when state is " + currentState);
}
}
// enum states {NONE, OBJECT_START, FIELD_NAME, INNER_OBJECT_FOUND, SCALAR_FOUND, LIST_FOUND,
// OBJECT_ENDS};
private boolean arrayEnds(states currentState) {
switch (currentState) {
case FIELD_NAME:// when empty array
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("Array ends called when state is " + currentState);
}
}
private boolean stringFound(states currentState) {
switch (currentState) {
case FIELD_NAME:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("stringFound called when state is " + currentState);
}
}
private boolean intFound(states currentState) {
switch (currentState) {
case FIELD_NAME:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("intFound called when state is " + currentState);
}
}
private boolean boolFound(states currentState) {
switch (currentState) {
case FIELD_NAME:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("boolFound called when state is " + currentState);
}
}
private boolean doubleFound(states currentState) {
switch (currentState) {
case FIELD_NAME:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("doubleFound called when state is " + currentState);
}
}
private boolean fieldFound(states currentState) {
switch (currentState) {
case OBJECT_START:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("fieldFound called when state is " + currentState);
}
}
private boolean nullFound(states currentState) {
switch (currentState) {
case FIELD_NAME:
case SCALAR_FOUND:
case LIST_FOUND:
case LIST_ENDS:
case OBJECT_ENDS:
return true;
default:
throw new IllegalStateException("nullFound called when state is " + currentState);
}
}
}