/* * (C) Copyright 2013 Kurento (http://kurento.org/) * * Licensed 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.kurento.jsonrpc; import static org.kurento.jsonrpc.internal.JsonRpcConstants.DATA_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.ERROR_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.ID_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.JSON_RPC_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.METHOD_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.PARAMS_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.RESULT_PROPERTY; import static org.kurento.jsonrpc.internal.JsonRpcConstants.SESSION_ID_PROPERTY; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import org.kurento.jsonrpc.internal.JsonRpcConstants; import org.kurento.jsonrpc.message.Message; import org.kurento.jsonrpc.message.Request; import org.kurento.jsonrpc.message.Response; import org.kurento.jsonrpc.message.ResponseError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.JsonSyntaxException; import com.google.gson.internal.$Gson$Types; /** * * Gson/JSON utilities; used to serialise Java object to JSON (as String). * * @author Miguel ParĂ­s (mparisdiaz@gsyc.es) * @since 1.0.0 */ public class JsonUtils { public static final boolean INJECT_SESSION_ID = true; /** * Static instance of Gson object. */ private static Gson gson; /** * Serialise Java object to JSON (as String). * * @param obj * Java Object representing a JSON message to be serialized * @return Serialised JSON message (as String) */ public static String toJson(Object obj) { return getGson().toJson(obj); } public static JsonObject toJsonObject(Object obj) { // TODO Optimise this implementation if possible return fromJson(getGson().toJson(obj), JsonObject.class); } public static Message fromJsonMessage(String message) { JsonObject json = fromJson(message, JsonObject.class); if (json.has(METHOD_PROPERTY)) { return fromJsonRequest(json, JsonObject.class); } else { return fromJsonResponse(json, JsonElement.class); } } public static <T> Request<T> fromJsonRequest(String json, Class<T> paramsClass) { if (INJECT_SESSION_ID) { // TODO Optimise this implementation if possible return fromJsonRequestInject(fromJson(json, JsonObject.class), paramsClass); } return getGson().fromJson(json, $Gson$Types.newParameterizedTypeWithOwner(null, Request.class, paramsClass)); } public static <T> Response<T> fromJsonResponse(String json, Class<T> resultClass) { if (INJECT_SESSION_ID) { // TODO Optimise this implementation if possible return fromJsonResponseInject(fromJson(json, JsonObject.class), resultClass); } try { return getGson().fromJson(json, $Gson$Types.newParameterizedTypeWithOwner(null, Response.class, resultClass)); } catch (JsonSyntaxException e) { throw new JsonRpcException("Exception converting Json '" + json + "' to a JSON-RPC response with params as class " + resultClass.getName(), e); } } public static <T> Request<T> fromJsonRequest(JsonObject json, Class<T> paramsClass) { if (INJECT_SESSION_ID) { // TODO Optimise this implementation if possible return fromJsonRequestInject(json, paramsClass); } return getGson().fromJson(json, $Gson$Types.newParameterizedTypeWithOwner(null, Request.class, paramsClass)); } public static <T> Response<T> fromJsonResponse(JsonObject json, Class<T> resultClass) { if (INJECT_SESSION_ID) { // TODO Optimize this implementation if possible return fromJsonResponseInject(json, resultClass); } return getGson().fromJson(json, $Gson$Types.newParameterizedTypeWithOwner(null, Response.class, resultClass)); } private static <T> Response<T> fromJsonResponseInject(JsonObject jsonObject, Class<T> resultClass) { try { String sessionId = extractSessionId(jsonObject, RESULT_PROPERTY); Response<T> response; if (resultClass != null) { response = JsonUtils.fromJson(jsonObject, $Gson$Types.newParameterizedTypeWithOwner(null, Response.class, resultClass)); } else { response = JsonUtils.fromJson(jsonObject, $Gson$Types.newParameterizedTypeWithOwner(null, Response.class, JsonElement.class)); } response.setSessionId(sessionId); return response; } catch (JsonSyntaxException e) { throw new JsonRpcException("Exception converting Json '" + jsonObject + "' to a JSON-RPC response with params as class " + resultClass.getName(), e); } } private static <T> Request<T> fromJsonRequestInject(JsonObject jsonObject, Class<T> paramsClass) { String sessionId = extractSessionId(jsonObject, PARAMS_PROPERTY); Request<T> request = getGson().fromJson(jsonObject, $Gson$Types.newParameterizedTypeWithOwner(null, Request.class, paramsClass)); request.setSessionId(sessionId); return request; } private static String extractSessionId(JsonObject jsonObject, String memberName) { JsonElement responseJson = jsonObject.get(memberName); if (responseJson != null && responseJson.isJsonObject()) { JsonObject responseJsonObject = (JsonObject) responseJson; JsonElement sessionIdJson = responseJsonObject.remove(SESSION_ID_PROPERTY); if (sessionIdJson != null && !(sessionIdJson instanceof JsonNull)) { return sessionIdJson.getAsString(); } } return null; } public static String toJson(Object obj, Type type) { return getGson().toJson(obj, type); } public static <T> String toJsonRequest(Request<T> request) { return getGson().toJson(request, $Gson$Types.newParameterizedTypeWithOwner(null, Request.class, getClassOrNull(request.getParams()))); } public static <T> String toJsonResponse(Response<T> request) { return getGson().toJson(request, $Gson$Types.newParameterizedTypeWithOwner(null, Response.class, getClassOrNull(request.getResult()))); } public static <T> T fromJson(String json, Class<T> clazz) { return getGson().fromJson(json, clazz); } public static <T> T fromJson(JsonElement json, Class<T> clazz) { return getGson().fromJson(json, clazz); } public static <T> T fromJson(String json, Type type) { return getGson().fromJson(json, type); } public static <T> T fromJson(JsonElement json, Type type) { return getGson().fromJson(json, type); } private static Class<?> getClassOrNull(Object object) { return object == null ? null : object.getClass(); } /** * Gson object accessor (getter). * * @return son object */ public static Gson getGson() { if (gson == null) { synchronized (JsonUtils.class) { if (gson == null) { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Request.class, new JsonRpcRequestDeserializer()); builder.registerTypeAdapter(Response.class, new JsonRpcResponseDeserializer()); builder.registerTypeAdapter(Props.class, new JsonPropsAdapter()); builder.disableHtmlEscaping(); gson = builder.create(); } } } return gson; } static boolean isIn(JsonObject jObject, String[] clues) { for (String clue : clues) { if (jObject.has(clue)) { return true; } } return false; } public static String toJsonMessage(Message message) { if (message.getSessionId() != null && INJECT_SESSION_ID) { JsonObject jsonObject = JsonUtils.toJsonObject(message); JsonObject objectToInjectSessionId; if (message instanceof Request) { objectToInjectSessionId = convertToObject(jsonObject, PARAMS_PROPERTY); } else { Response<?> response = (Response<?>) message; if (response.getError() == null) { objectToInjectSessionId = convertToObject(jsonObject, RESULT_PROPERTY); } else { objectToInjectSessionId = convertToObject(jsonObject, ERROR_PROPERTY, DATA_PROPERTY); } } objectToInjectSessionId.addProperty(JsonRpcConstants.SESSION_ID_PROPERTY, message.getSessionId()); return jsonObject.toString(); } return JsonUtils.toJson(message); } private static JsonObject convertToObject(JsonObject jsonObject, String... properties) { String property = properties[0]; JsonElement paramsJson = jsonObject.get(property); JsonObject paramsAsObject = null; if (paramsJson == null) { paramsAsObject = new JsonObject(); jsonObject.add(property, paramsAsObject); paramsJson = paramsAsObject; } if (!paramsJson.isJsonObject()) { paramsAsObject = new JsonObject(); paramsAsObject.add("value", paramsJson); jsonObject.add(property, paramsAsObject); } else { paramsAsObject = (JsonObject) paramsJson; } if (properties.length > 1) { convertToObject(jsonObject, Arrays.copyOfRange(properties, 1, properties.length)); } return paramsAsObject; } public static JsonElement toJsonElement(Object object) { return getGson().toJsonTree(object); } public static <E> E extractJavaValueFromResult(JsonElement result, Type type) { if (type == Void.class || type == void.class) { return null; } JsonElement extractResult = extractJsonValueFromResponse(result, type); return JsonUtils.fromJson(extractResult, type); } private static JsonElement extractJsonValueFromResponse(JsonElement result, Type type) { if (result == null) { return null; } if (isPrimitiveClass(type) || isEnum(type)) { if (result instanceof JsonPrimitive) { return result; } else if (result instanceof JsonArray) { throw new JsonRpcException( "Json array '" + result + " cannot be converted to " + getTypeName(type)); } else if (result instanceof JsonObject) { return extractSimpleValueFromJsonObject((JsonObject) result, type); } else { throw new JsonRpcException("Unrecognized json element: " + result); } } else if (isList(type)) { if (result instanceof JsonArray) { return result; } return extractSimpleValueFromJsonObject((JsonObject) result, type); } else { return result; } } private static JsonElement extractSimpleValueFromJsonObject(JsonObject result, Type type) { if (!result.has("value")) { throw new JsonRpcException("Json object " + result + " cannot be converted to " + getTypeName(type) + " without a 'value' property"); } return result.get("value"); } private static boolean isEnum(Type type) { if (type instanceof Class) { Class<?> clazz = (Class<?>) type; return clazz.isEnum(); } return false; } private static boolean isPrimitiveClass(Type type) { return type == String.class || type == Integer.class || type == Float.class || type == Boolean.class || type == int.class || type == float.class || type == boolean.class; } private static boolean isList(Type type) { if (type == List.class) { return true; } if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; if (pType.getRawType() instanceof Class) { return ((Class<?>) pType.getRawType()).isAssignableFrom(List.class); } } return false; } private static String getTypeName(Type type) { if (type instanceof Class) { Class<?> clazz = (Class<?>) type; return clazz.getSimpleName(); } else if (type instanceof ParameterizedType) { StringBuilder sb = new StringBuilder(); ParameterizedType pType = (ParameterizedType) type; Class<?> rawClass = (Class<?>) pType.getRawType(); sb.append(rawClass.getSimpleName()); Type[] arguments = pType.getActualTypeArguments(); if (arguments.length > 0) { sb.append('<'); for (Type aType : arguments) { sb.append(getTypeName(aType)); sb.append(','); } sb.deleteCharAt(sb.length() - 1); sb.append('>'); } return sb.toString(); } return type.toString(); } public static List<String> toStringList(JsonArray values) { List<String> list = new ArrayList<>(); for (JsonElement element : values) { if (element instanceof JsonPrimitive) { list.add(((JsonPrimitive) element).getAsString()); } else { throw new JsonParseException("JsonArray " + values + " contains non string elements"); } } return list; } } class JsonRpcResponseDeserializer implements JsonDeserializer<Response<?>> { private static final Logger log = LoggerFactory.getLogger(JsonRpcResponseDeserializer.class); @Override public Response<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (!(json instanceof JsonObject)) { throw new JsonParseException("JonObject expected, found " + json.getClass().getSimpleName()); } JsonObject jObject = (JsonObject) json; if (!jObject.has(JSON_RPC_PROPERTY)) { throw new JsonParseException( "Invalid JsonRpc response lacking version '" + JSON_RPC_PROPERTY + "' field"); } if (!jObject.get(JSON_RPC_PROPERTY).getAsString().equals(JsonRpcConstants.JSON_RPC_VERSION)) { throw new JsonParseException("Invalid JsonRpc version"); } Integer id = null; JsonElement idAsJsonElement = jObject.get(ID_PROPERTY); if (idAsJsonElement != null) { try { id = Integer.valueOf(idAsJsonElement.getAsInt()); } catch (Exception e) { throw new JsonParseException( "Invalid format in '" + ID_PROPERTY + "' field in request " + json); } } if (jObject.has(ERROR_PROPERTY)) { return new Response<>(id, (ResponseError) context.deserialize(jObject.get(ERROR_PROPERTY), ResponseError.class)); } else { if (jObject.has(RESULT_PROPERTY)) { ParameterizedType parameterizedType = (ParameterizedType) typeOfT; return new Response<>(id, context.deserialize(jObject.get(RESULT_PROPERTY), parameterizedType.getActualTypeArguments()[0])); } else { log.warn("Invalid JsonRpc response: " + json + " It lacks a valid '" + RESULT_PROPERTY + "' or '" + ERROR_PROPERTY + "' field"); return new Response<>(id, null); } } } } class JsonRpcRequestDeserializer implements JsonDeserializer<Request<?>> { @Override public Request<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (!(json instanceof JsonObject)) { throw new JsonParseException( "Invalid JsonRpc request showning JsonElement type " + json.getClass().getSimpleName()); } JsonObject jObject = (JsonObject) json; // FIXME: Enable again when KMS sends jsonrpc field in register message // if (!jObject.has(JSON_RPC_PROPERTY)) { // throw new JsonParseException( // "Invalid JsonRpc request lacking version '" // + JSON_RPC_PROPERTY + "' field"); // } // // if (!jObject.get("jsonrpc").getAsString().equals(JSON_RPC_VERSION)) { // throw new JsonParseException("Invalid JsonRpc version"); // } if (!jObject.has(METHOD_PROPERTY)) { throw new JsonParseException( "Invalid JsonRpc request lacking '" + METHOD_PROPERTY + "' field"); } Integer id = null; if (jObject.has(ID_PROPERTY)) { id = Integer.valueOf(jObject.get(ID_PROPERTY).getAsInt()); } ParameterizedType parameterizedType = (ParameterizedType) typeOfT; return new Request<>(id, jObject.get(METHOD_PROPERTY).getAsString(), context .deserialize(jObject.get(PARAMS_PROPERTY), parameterizedType.getActualTypeArguments()[0])); } } class JsonPropsAdapter implements JsonDeserializer<Props>, JsonSerializer<Props> { @Override public Props deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (!(json instanceof JsonObject)) { throw new JsonParseException("Cannot convert " + json + " to Props object"); } JsonObject jObject = (JsonObject) json; Props props = new Props(); for (Map.Entry<String, JsonElement> e : jObject.entrySet()) { Object value = deserialize(e.getValue(), context); props.add(e.getKey(), value); } return props; } private Object deserialize(JsonElement value, JsonDeserializationContext context) { if (value instanceof JsonObject) { return deserialize(value, null, context); } else if (value instanceof JsonPrimitive) { return toPrimitiveObject(value); } else if (value instanceof JsonArray) { JsonArray array = (JsonArray) value; List<Object> result = new ArrayList<>(); for (JsonElement element : array) { result.add(deserialize(element, context)); } return result; } else if (value instanceof JsonNull) { return null; } else { throw new JsonRpcException("Unrecognized Json element: " + value); } } public Object toPrimitiveObject(JsonElement element) { JsonPrimitive primitive = (JsonPrimitive) element; if (primitive.isBoolean()) { return Boolean.valueOf(primitive.getAsBoolean()); } else if (primitive.isNumber()) { Number number = primitive.getAsNumber(); double value = number.doubleValue(); if ((int) value == value) { return Integer.valueOf((int) value); } return Float.valueOf((float) value); } else if (primitive.isString()) { return primitive.getAsString(); } else { throw new JsonRpcException("Unrecognized JsonPrimitive: " + primitive); } } @Override public JsonElement serialize(Props props, Type typeOfSrc, JsonSerializationContext context) { JsonObject jsonObject = new JsonObject(); for (Prop prop : props) { jsonObject.add(prop.getName(), context.serialize(prop.getValue())); } return jsonObject; } }