| /******************************************************************************* |
| * 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; |
| } |
| } |