/* * Copyright 2000-2016 Vaadin Ltd. * * 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 com.vaadin.client.communication; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.JsArrayObject; import com.vaadin.client.metadata.NoDataException; import com.vaadin.client.metadata.Property; import com.vaadin.client.metadata.Type; import com.vaadin.shared.Connector; import com.vaadin.shared.JsonConstants; import com.vaadin.shared.communication.UidlValue; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; import elemental.json.JsonValue; /** * Encoder for converting RPC parameters and other values to JSON for transfer * between the client and the server. * * Currently, basic data types as well as Map, String[] and Object[] are * supported, where maps and Object[] can contain other supported data types. * * TODO extensible type support * * @since 7.0 */ public class JsonEncoder { /** * Encode a value to a JSON representation for transport from the client to * the server. * * @param value * value to convert * @param connection * @return JSON representation of the value */ public static JsonValue encode(Object value, Type type, ApplicationConnection connection) { if (null == value) { return Json.createNull(); } else if (value instanceof JsonValue) { return (JsonValue) value; } else if (value instanceof String[]) { String[] array = (String[]) value; JsonArray jsonArray = Json.createArray(); for (int i = 0; i < array.length; ++i) { jsonArray.set(i, array[i]); } return jsonArray; } else if (value instanceof String) { return Json.create((String) value); } else if (value instanceof Boolean) { return Json.create((Boolean) value); } else if (value instanceof Number) { return Json.create(((Number) value).doubleValue()); } else if (value instanceof Character) { return Json.create(String.valueOf(value)); } else if (value instanceof Object[] && type == null) { // Non-legacy arrays handed by generated serializer return encodeLegacyObjectArray((Object[]) value, connection); } else if (value instanceof Enum) { return encodeEnum((Enum<?>) value, connection); } else if (value instanceof Map) { return encodeMap((Map) value, type, connection); } else if (value instanceof Connector) { Connector connector = (Connector) value; return Json.create(connector.getConnectorId()); } else if (value instanceof Collection) { return encodeCollection((Collection) value, type, connection); } else if (value instanceof UidlValue) { return encodeVariableChange((UidlValue) value, connection); } else { // First see if there's a custom serializer JSONSerializer<Object> serializer = null; if (type != null) { serializer = (JSONSerializer<Object>) type.findSerializer(); if (serializer != null) { return serializer.serialize(value, connection); } } String transportType = getTransportType(value); if (transportType != null) { // Send the string value for remaining legacy types return Json.create(String.valueOf(value)); } else if (type != null) { // And finally try using bean serialization logic try { JsArrayObject<Property> properties = type .getPropertiesAsArray(); JsonObject jsonObject = Json.createObject(); int size = properties.size(); for (int i = 0; i < size; i++) { Property property = properties.get(i); Object propertyValue = property.getValue(value); Type propertyType = property.getType(); JsonValue encodedPropertyValue = encode(propertyValue, propertyType, connection); jsonObject.put(property.getName(), encodedPropertyValue); } return jsonObject; } catch (NoDataException e) { throw new RuntimeException( "Can not encode " + type.getSignature(), e); } } else { throw new RuntimeException("Can't encode " + value.getClass() + " without type information"); } } } private static JsonValue encodeVariableChange(UidlValue uidlValue, ApplicationConnection connection) { Object value = uidlValue.getValue(); JsonArray jsonArray = Json.createArray(); String transportType = getTransportType(value); if (transportType == null) { /* * This should not happen unless you try to send an unsupported type * in a legacy variable change from the client to the server. */ String valueType = null; if (value != null) { valueType = value.getClass().getName(); } throw new IllegalArgumentException( "Cannot encode object of type " + valueType); } jsonArray.set(0, Json.create(transportType)); jsonArray.set(1, encode(value, null, connection)); return jsonArray; } private static JsonValue encodeMap(Map<Object, Object> map, Type type, ApplicationConnection connection) { /* * As we have no info about declared types, we instead select encoding * scheme based on actual type of first key. We can't do this if there's * no first key, so instead we send some special value that the * server-side decoding must check for. (see #8906) */ if (map.isEmpty()) { return Json.createArray(); } Object firstKey = map.keySet().iterator().next(); if (firstKey instanceof String) { return encodeStringMap(map, type, connection); } else if (type == null) { throw new IllegalStateException( "Only string keys supported for legacy maps"); } else if (firstKey instanceof Connector) { return encodeConnectorMap(map, type, connection); } else { return encodeObjectMap(map, type, connection); } } private static JsonValue encodeChildValue(Object value, Type collectionType, int typeIndex, ApplicationConnection connection) { if (collectionType == null) { return encode(new UidlValue(value), null, connection); } else { assert collectionType.getParameterTypes() != null && collectionType.getParameterTypes().length > typeIndex && collectionType .getParameterTypes()[typeIndex] != null : "Proper generics required for encoding child value, assertion failed for " + collectionType; Type childType = collectionType.getParameterTypes()[typeIndex]; return encode(value, childType, connection); } } private static JsonArray encodeObjectMap(Map<Object, Object> map, Type type, ApplicationConnection connection) { JsonArray keys = Json.createArray(); JsonArray values = Json.createArray(); assert type != null : "Should only be used for non-legacy types"; for (Entry<?, ?> entry : map.entrySet()) { keys.set(keys.length(), encodeChildValue(entry.getKey(), type, 0, connection)); values.set(values.length(), encodeChildValue(entry.getValue(), type, 1, connection)); } JsonArray keysAndValues = Json.createArray(); keysAndValues.set(0, keys); keysAndValues.set(1, values); return keysAndValues; } private static JsonValue encodeConnectorMap(Map<Object, Object> map, Type type, ApplicationConnection connection) { JsonObject jsonMap = Json.createObject(); for (Entry<?, ?> entry : map.entrySet()) { Connector connector = (Connector) entry.getKey(); JsonValue encodedValue = encodeChildValue(entry.getValue(), type, 1, connection); jsonMap.put(connector.getConnectorId(), encodedValue); } return jsonMap; } private static JsonValue encodeStringMap(Map<Object, Object> map, Type type, ApplicationConnection connection) { JsonObject jsonMap = Json.createObject(); for (Entry<?, ?> entry : map.entrySet()) { String key = (String) entry.getKey(); Object value = entry.getValue(); jsonMap.put(key, encodeChildValue(value, type, 1, connection)); } return jsonMap; } private static JsonValue encodeEnum(Enum<?> e, ApplicationConnection connection) { return Json.create(e.toString()); } private static JsonValue encodeLegacyObjectArray(Object[] array, ApplicationConnection connection) { JsonArray jsonArray = Json.createArray(); for (int i = 0; i < array.length; ++i) { // TODO handle object graph loops? Object value = array[i]; jsonArray.set(i, encode(value, null, connection)); } return jsonArray; } private static JsonArray encodeCollection(Collection collection, Type type, ApplicationConnection connection) { JsonArray jsonArray = Json.createArray(); int idx = 0; for (Object o : collection) { JsonValue encodedObject = encodeChildValue(o, type, 0, connection); jsonArray.set(idx++, encodedObject); } if (collection instanceof Set) { return jsonArray; } else if (collection instanceof List) { return jsonArray; } else { throw new RuntimeException("Unsupport collection type: " + collection.getClass().getName()); } } /** * Returns the transport type for the given value. Only returns a transport * type for internally handled values. * * @param value * The value that should be transported * @return One of the JsonEncode.VTYPE_ constants or null if the value * cannot be transported using an internally handled type. */ private static String getTransportType(Object value) { if (value == null) { return JsonConstants.VTYPE_NULL; } else if (value instanceof String) { return JsonConstants.VTYPE_STRING; } else if (value instanceof Connector) { return JsonConstants.VTYPE_CONNECTOR; } else if (value instanceof Boolean) { return JsonConstants.VTYPE_BOOLEAN; } else if (value instanceof Integer) { return JsonConstants.VTYPE_INTEGER; } else if (value instanceof Float) { return JsonConstants.VTYPE_FLOAT; } else if (value instanceof Double) { return JsonConstants.VTYPE_DOUBLE; } else if (value instanceof Long) { return JsonConstants.VTYPE_LONG; } else if (value instanceof List) { return JsonConstants.VTYPE_LIST; } else if (value instanceof Set) { return JsonConstants.VTYPE_SET; } else if (value instanceof String[]) { return JsonConstants.VTYPE_STRINGARRAY; } else if (value instanceof Object[]) { return JsonConstants.VTYPE_ARRAY; } else if (value instanceof Map) { return JsonConstants.VTYPE_MAP; } else if (value instanceof Enum<?>) { // Enum value is processed as a string return JsonConstants.VTYPE_STRING; } return null; } }