blob: fde9ef9915e1010f6e80cd4a908fece818cca6e5 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2021 the Eclipse BaSyx Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
******************************************************************************/
package org.eclipse.basyx.vab.coder.json.serialization;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
/**
* Provides means for (de-)serialization of Primitives (int, double, string,
* boolean), Maps, Sets and Lists. <br />
* Since JSON is not able to differentiate between Sets and Lists, additional
* information is added. When a Collection of objects is serialized, this
* information is directly added using an "index" key. <br />
* However, collections of primitives do not allow adding an "index" key. To
* handle this, a type tag is added on the same level as the collection. For
* more details, see <i>TestJson</i>
*
* @author rajashek, schnicke
*
*/
public class GSONTools implements Serializer {
private static Logger logger = LoggerFactory.getLogger(GSONTools.class);
// Used string constants
public static final String OPERATION = "operation";
public static final String LAMBDA = "lambda";
public static final String BASYXFUNCTIONTYPE = "_basyxFunctionType";
public static final String BASYXINVOCABLE = "_basyxInvocable";
public static final String BASYXFUNCTIONVALUE = "_basyxFunctionValue";
/**
* JsonParser reference
*/
protected static JsonParser parser = new JsonParser();
/**
* Type factory
*/
protected GSONToolsFactory toolsFactory = null;
/**
* Flag to remove null values from serialized JSON
*/
private boolean removeNull = true;
/**
* Flag to remove empty arrays from serialized JSON
*/
private boolean removeEmpty = false;
/**
* Constructor
*/
public GSONTools(GSONToolsFactory factory) {
// Store factory reference
toolsFactory = factory;
}
/**
* Constructor
*/
public GSONTools(GSONToolsFactory factory, boolean removeNull, boolean removeEmpty) {
this(factory);
this.removeNull = removeNull;
this.removeEmpty = removeEmpty;
}
/**
* Set factory instance
*/
public void setFactory(GSONToolsFactory newFactoryInstance) {
// Store factory instance
toolsFactory = newFactoryInstance;
}
@Override
public Object deserialize(String str) {
JsonElement elem = parser.parse(str);
return deserializeJsonElement(elem);
}
@Override
public String serialize(Object obj) {
JsonElement elem = serializeObject(obj);
// Removing null value if the removeNull flag is on
if (removeNull) {
// Gson#toJson removes null automatically
Gson gson = new Gson();
return gson.toJson(elem);
} else {
return elem.toString();
}
}
/**
* Serialized an arbitrary object to a JsonElement
*
* @param obj
* @return
*/
@SuppressWarnings("unchecked")
private JsonElement serializeObject(Object obj) {
if (obj == null) {
return JsonNull.INSTANCE;
} else if (obj.getClass().isPrimitive() || isWrapperType(obj.getClass()) || obj instanceof String || obj instanceof Number) {
return serializePrimitive(obj);
} else if (obj instanceof Map<?, ?>) {
return serializeMap((Map<String, Object>) obj);
} else if (obj instanceof Collection<?>) {
return serializeCollection((Collection<Object>) obj);
} else if (isFunction(obj)) {
return serializeFunction(obj);
}
throw new RuntimeException("Unknown element!");
}
/**
* Deserializes a JsonElement to an object
*
* @param elem
* @return
*/
private Object deserializeJsonElement(JsonElement elem) {
if (elem.isJsonPrimitive()) {
return deserializeJsonPrimitive(elem.getAsJsonPrimitive());
} else if (elem.isJsonObject()) {
return deserializeJsonObject(elem.getAsJsonObject());
} else if (elem.isJsonArray()) {
return deserializeJsonArray(elem.getAsJsonArray());
}
return null;
}
/**
* Indicates if a class is a wrapper type, e.g. <i>Integer</i> for <i>int</i>
*
* @param clazz
* @return
*/
private static boolean isWrapperType(Class<?> clazz) {
return clazz.equals(Boolean.class) || clazz.equals(Integer.class) || clazz.equals(Character.class) || clazz.equals(Byte.class) || clazz.equals(Short.class) || clazz.equals(Double.class) || clazz.equals(Long.class)
|| clazz.equals(Float.class);
}
/**
* Deserializes a JsonPrimitive to either string, int, double or boolean
*
* @param primitive
* @return
*/
private Object deserializeJsonPrimitive(JsonPrimitive primitive) {
if (primitive.isNumber()) {
if (primitive.getAsString().contains(".")) {
return primitive.getAsDouble();
} else {
// Get value as Big integer
BigInteger tmp= primitive.getAsBigInteger();
if (BigInteger.valueOf(Integer.MAX_VALUE).compareTo(tmp) >= 0 && BigInteger.valueOf(Integer.MIN_VALUE).compareTo(tmp) <= 0) {
// convert to int
return primitive.getAsInt();
} else if (BigInteger.valueOf(Long.MAX_VALUE).compareTo(tmp) >= 0 && BigInteger.valueOf(Long.MIN_VALUE).compareTo(tmp) <= 0) {
// convert to long
return primitive.getAsLong();
} else {
// for types NonNegativeInteger, NonPositiveInteger, NegativeInteger,
// PositiveInteger
return tmp;
}
}
} else if (primitive.isBoolean()) {
return primitive.getAsBoolean();
} else {
return primitive.getAsString();
}
}
/**
* Serializes either string, number or boolean to a JsonPrimitive
*
* @param primitive
* @return
*/
private JsonPrimitive serializePrimitive(Object primitive) {
if (primitive instanceof Number) {
return new JsonPrimitive((Number) primitive);
} else if (primitive instanceof Boolean) {
return new JsonPrimitive((Boolean) primitive);
} else {
return new JsonPrimitive((String) primitive);
}
}
/**
* Deserializes a JsonObject to either a map, an operations or an arbitrary
* serializable object
*
* @param map
* @return
*/
private Object deserializeJsonObject(JsonObject map) {
if (map.has(BASYXFUNCTIONTYPE)) {
String functionType = map.get(BASYXFUNCTIONTYPE).getAsString();
if (functionType.equals(OPERATION)) {
return BASYXINVOCABLE;
} else if (functionType.equals(LAMBDA)) {
return deserializeObjectFromString(map.get(BASYXFUNCTIONVALUE).getAsString());
} else {
throw new RuntimeException("Unknown function type " + functionType + "!");
}
} else {
return deserializeObject(map);
}
}
/**
* Deserializes a JsonObject to a map
*
* @param map
* @return
*/
private Map<String, Object> deserializeObject(JsonObject map) {
Map<String, Object> ret = toolsFactory.createMap();
for (String k : map.keySet()) {
ret.put(k, deserializeJsonElement(map.get(k)));
}
return ret;
}
/**
* Serializes a Map to a JsonObject
*
* @param map
* @return
*/
private JsonObject serializeMap(Map<String, Object> map) {
JsonObject obj = new JsonObject();
for (Entry<String, Object> entry : map.entrySet()) {
Object value = entry.getValue();
// Remove empty list if removeEmpty flag is on
if (!removeEmpty || !(value instanceof Collection<?> && ((Collection<?>)value).isEmpty())) {
obj.add(entry.getKey(), serializeObject(value));
}
}
return obj;
}
/**
* Deserializes a JsonArray to a Collection<br/>
* Remark: internally, a List will be used for deserialization & it is assumed, that
* the order in the json equals the correct intended order for the list.
* => The ordering will be preserved in the returned collection
*
* @param array
* @return
*/
private Collection<Object> deserializeJsonArray(JsonArray array) {
Collection<Object> list = toolsFactory.createCollection();
for (JsonElement element : array) {
list.add(deserializeJsonElement(element));
}
return list;
}
/**
* Checks if an object is a lambda function
*
* @param value
* @return
*/
private boolean isFunction(Object value) {
return (value instanceof Supplier<?>)
|| (value instanceof Function<?, ?>)
|| (value instanceof Consumer<?>)
|| (value instanceof BiConsumer<?, ?>);
}
/**
* Serializes a list to a JsonArray and adds index where appropriate
*
* @param list
* @return
*/
private JsonArray serializeCollection(Collection<Object> collection) {
JsonArray array = new JsonArray();
collection.stream().map(this::serializeObject).forEach(array::add);
return array;
}
/**
* Serializes a function if possible
*
* @param function
* @return
*/
private JsonObject serializeFunction(Object function) {
if (function instanceof Serializable) {
return serializeSerializableOperation((Serializable) function);
} else {
return serializeNotSerializableOperation(function);
}
}
/**
* Read an object from Base64 string.
*/
protected Object deserializeObjectFromString(String s) {
Object result = null;
byte[] data = Base64.getDecoder().decode(s);
InputStream byteStream = new ByteArrayInputStream(data);
try (ObjectInputStream stream = new ObjectInputStream(byteStream)) {
result = stream.readObject();
} catch (IOException | ClassNotFoundException e) {
logger.error("Exception in deserializeObjectFromString", e);
}
return result;
}
/**
* Write the object to a Base64 string.
*/
protected String serializeObjectToString(Serializable obj) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(outStream)) {
oos.writeObject(obj);
} catch (IOException e) {
logger.error("Exception in serializeObjectToString", e);
}
byte[] data = outStream.toByteArray();
return Base64.getEncoder().encodeToString(data);
}
/**
* Serialize an operation descriptor
*/
private JsonObject serializeSerializableOperation(Serializable value) {
JsonObject target = new JsonObject();
// Serializable functions will be serialized.
target.add(BASYXFUNCTIONTYPE, new JsonPrimitive(LAMBDA));
String serialized = serializeObjectToString(value);
target.add(BASYXFUNCTIONVALUE, new JsonPrimitive(serialized));
return target;
}
/**
* Serializes a NonSerializableOperation to a String indicating that fact
*
* @param function
* @return
*/
private JsonObject serializeNotSerializableOperation(Object function) {
JsonObject target = new JsonObject();
// Not serializable functions will be not be serialized.
target.add(BASYXFUNCTIONTYPE, new JsonPrimitive(OPERATION));
return target;
}
}