com.vaadin.server.JsonCodec Maven / Gradle / Ivy
/*
* Copyright 2000-2014 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.server;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.vaadin.server.communication.DateSerializer;
import com.vaadin.server.communication.JSONSerializer;
import com.vaadin.shared.Connector;
import com.vaadin.shared.JsonConstants;
import com.vaadin.shared.communication.UidlValue;
import com.vaadin.ui.Component;
import com.vaadin.ui.ConnectorTracker;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonException;
import elemental.json.JsonNull;
import elemental.json.JsonObject;
import elemental.json.JsonString;
import elemental.json.JsonType;
import elemental.json.JsonValue;
import elemental.json.impl.JreJsonArray;
/**
* Decoder for converting RPC parameters and other values from JSON in transfer
* between the client and the server and vice versa.
*
* @since 7.0
*/
public class JsonCodec implements Serializable {
/* Immutable Encode Result representing null */
private static final EncodeResult ENCODE_RESULT_NULL = new EncodeResult(
Json.createNull());
/* Immutable empty JSONArray */
private static final JsonArray EMPTY_JSON_ARRAY = new JreJsonArray(
Json.instance()) {
@Override
public void set(int index, JsonValue value) {
throw new UnsupportedOperationException(
"Immutable empty JsonArray.");
}
@Override
public void set(int index, String string) {
throw new UnsupportedOperationException(
"Immutable empty JsonArray.");
}
@Override
public void set(int index, double number) {
throw new UnsupportedOperationException(
"Immutable empty JsonArray.");
}
@Override
public void set(int index, boolean bool) {
throw new UnsupportedOperationException(
"Immutable empty JsonArray.");
}
};
public static interface BeanProperty extends Serializable {
public Object getValue(Object bean) throws Exception;
public void setValue(Object bean, Object value) throws Exception;
public String getName();
public Type getType();
}
private static class FieldProperty implements BeanProperty {
private final Field field;
public FieldProperty(Field field) {
this.field = field;
}
@Override
public Object getValue(Object bean) throws Exception {
return field.get(bean);
}
@Override
public void setValue(Object bean, Object value) throws Exception {
field.set(bean, value);
}
@Override
public String getName() {
return field.getName();
}
@Override
public Type getType() {
return field.getGenericType();
}
public static Collection find(Class> type)
throws IntrospectionException {
Field[] fields = type.getFields();
Collection properties = new ArrayList(
fields.length);
for (Field field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {
properties.add(new FieldProperty(field));
}
}
return properties;
}
}
private static class MethodProperty implements BeanProperty {
private final PropertyDescriptor pd;
public MethodProperty(PropertyDescriptor pd) {
this.pd = pd;
}
@Override
public Object getValue(Object bean) throws Exception {
Method readMethod = pd.getReadMethod();
return readMethod.invoke(bean);
}
@Override
public void setValue(Object bean, Object value) throws Exception {
pd.getWriteMethod().invoke(bean, value);
}
@Override
public String getName() {
String fieldName = pd.getWriteMethod().getName().substring(3);
fieldName = Character.toLowerCase(fieldName.charAt(0))
+ fieldName.substring(1);
return fieldName;
}
public static Collection find(Class> type)
throws IntrospectionException {
Collection properties = new ArrayList();
for (PropertyDescriptor pd : Introspector.getBeanInfo(type)
.getPropertyDescriptors()) {
if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
continue;
}
properties.add(new MethodProperty(pd));
}
return properties;
}
@Override
public Type getType() {
return pd.getReadMethod().getGenericReturnType();
}
}
/**
* Cache the collection of bean properties for a given type to avoid doing a
* quite expensive lookup multiple times. Will be used from any thread that
* happens to process Vaadin requests, so it must be protected from
* corruption caused by concurrent access.
*/
private static ConcurrentMap, Collection> typePropertyCache = new ConcurrentHashMap, Collection>();
private static Map, String> typeToTransportType = new HashMap, String>();
/**
* Note! This does not contain primitives.
*
*/
private static Map> transportTypeToType = new HashMap>();
private static Map, JSONSerializer>> customSerializers = new HashMap, JSONSerializer>>();
static {
customSerializers.put(Date.class, new DateSerializer());
}
static {
registerType(String.class, JsonConstants.VTYPE_STRING);
registerType(Connector.class, JsonConstants.VTYPE_CONNECTOR);
registerType(Boolean.class, JsonConstants.VTYPE_BOOLEAN);
registerType(boolean.class, JsonConstants.VTYPE_BOOLEAN);
registerType(Integer.class, JsonConstants.VTYPE_INTEGER);
registerType(int.class, JsonConstants.VTYPE_INTEGER);
registerType(Float.class, JsonConstants.VTYPE_FLOAT);
registerType(float.class, JsonConstants.VTYPE_FLOAT);
registerType(Double.class, JsonConstants.VTYPE_DOUBLE);
registerType(double.class, JsonConstants.VTYPE_DOUBLE);
registerType(Long.class, JsonConstants.VTYPE_LONG);
registerType(long.class, JsonConstants.VTYPE_LONG);
registerType(String[].class, JsonConstants.VTYPE_STRINGARRAY);
registerType(Object[].class, JsonConstants.VTYPE_ARRAY);
registerType(Map.class, JsonConstants.VTYPE_MAP);
registerType(HashMap.class, JsonConstants.VTYPE_MAP);
registerType(List.class, JsonConstants.VTYPE_LIST);
registerType(Set.class, JsonConstants.VTYPE_SET);
registerType(Void.class, JsonConstants.VTYPE_NULL);
}
private static void registerType(Class> type, String transportType) {
typeToTransportType.put(type, transportType);
if (!type.isPrimitive()) {
transportTypeToType.put(transportType, type);
}
}
public static boolean isInternalTransportType(String transportType) {
return transportTypeToType.containsKey(transportType);
}
public static boolean isInternalType(Type type) {
if (type instanceof Class && ((Class>) type).isPrimitive()) {
if (type == byte.class || type == char.class) {
// Almost all primitive types are handled internally
return false;
}
// All primitive types are handled internally
return true;
} else if (type == UidlValue.class) {
// UidlValue is a special internal type wrapping type info and a
// value
return true;
}
return typeToTransportType.containsKey(getClassForType(type));
}
private static Class> getClassForType(Type type) {
if (type instanceof ParameterizedType) {
return (Class>) (((ParameterizedType) type).getRawType());
} else if (type instanceof Class>) {
return (Class>) type;
} else {
return null;
}
}
private static Class> getType(String transportType) {
return transportTypeToType.get(transportType);
}
public static Object decodeInternalOrCustomType(Type targetType,
JsonValue value, ConnectorTracker connectorTracker) {
if (isInternalType(targetType)) {
return decodeInternalType(targetType, false, value,
connectorTracker);
} else {
return decodeCustomType(targetType, value, connectorTracker);
}
}
public static Object decodeCustomType(Type targetType, JsonValue value,
ConnectorTracker connectorTracker) {
if (isInternalType(targetType)) {
throw new JsonException("decodeCustomType cannot be used for "
+ targetType + ", which is an internal type");
}
// Try to decode object using fields
if (isJsonType(targetType)) {
return value;
} else if (value.getType() == JsonType.NULL) {
return null;
} else if (targetType == byte.class || targetType == Byte.class) {
return Byte.valueOf((byte) value.asNumber());
} else if (targetType == char.class || targetType == Character.class) {
return Character.valueOf(value.asString().charAt(0));
} else if (targetType instanceof Class>
&& ((Class>) targetType).isArray()) {
// Legacy Object[] and String[] handled elsewhere, this takes care
// of generic arrays
Class> componentType = ((Class>) targetType).getComponentType();
return decodeArray(componentType, (JsonArray) value,
connectorTracker);
} else if (targetType instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) targetType)
.getGenericComponentType();
return decodeArray(componentType, (JsonArray) value,
connectorTracker);
} else if (JsonValue.class
.isAssignableFrom(getClassForType(targetType))) {
return value;
} else if (Enum.class.isAssignableFrom(getClassForType(targetType))) {
Class> classForType = getClassForType(targetType);
return decodeEnum(classForType.asSubclass(Enum.class),
(JsonString) value);
} else if (customSerializers.containsKey(getClassForType(targetType))) {
return customSerializers.get(getClassForType(targetType))
.deserialize(targetType, value, connectorTracker);
} else {
return decodeObject(targetType, (JsonObject) value,
connectorTracker);
}
}
private static boolean isJsonType(Type type) {
return type instanceof Class>
&& JsonValue.class.isAssignableFrom((Class>) type);
}
private static Object decodeArray(Type componentType, JsonArray value,
ConnectorTracker connectorTracker) {
Class> componentClass = getClassForType(componentType);
Object array = Array.newInstance(componentClass, value.length());
for (int i = 0; i < value.length(); i++) {
Object decodedValue = decodeInternalOrCustomType(componentType,
value.get(i), connectorTracker);
Array.set(array, i, decodedValue);
}
return array;
}
/**
* Decodes a value that is of an internal type.
*
* Ensures the encoded value is of the same type as target type.
*
*
* Allows restricting collections so that they must be declared using
* generics. If this is used then all objects in the collection are encoded
* using the declared type. Otherwise only internal types are allowed in
* collections.
*
*
* @param targetType
* The type that should be returned by this method
* @param valueAndType
* The encoded value and type array
* @param application
* A reference to the application
* @param enforceGenericsInCollections
* true if generics should be enforce, false to only allow
* internal types in collections
* @return
*/
public static Object decodeInternalType(Type targetType,
boolean restrictToInternalTypes, JsonValue encodedJsonValue,
ConnectorTracker connectorTracker) {
if (!isInternalType(targetType)) {
throw new JsonException("Type " + targetType
+ " is not a supported internal type.");
}
String transportType = getInternalTransportType(targetType);
if (encodedJsonValue.getType() == JsonType.NULL) {
return null;
} else if (targetType == Void.class) {
throw new JsonException(
"Something other than null was encoded for a null type");
}
// UidlValue
if (targetType == UidlValue.class) {
return decodeUidlValue((JsonArray) encodedJsonValue,
connectorTracker);
}
// Collections
if (JsonConstants.VTYPE_LIST.equals(transportType)) {
return decodeList(targetType, restrictToInternalTypes,
(JsonArray) encodedJsonValue, connectorTracker);
} else if (JsonConstants.VTYPE_SET.equals(transportType)) {
return decodeSet(targetType, restrictToInternalTypes,
(JsonArray) encodedJsonValue, connectorTracker);
} else if (JsonConstants.VTYPE_MAP.equals(transportType)) {
return decodeMap(targetType, restrictToInternalTypes,
encodedJsonValue, connectorTracker);
}
// Arrays
if (JsonConstants.VTYPE_ARRAY.equals(transportType)) {
return decodeObjectArray(targetType, (JsonArray) encodedJsonValue,
connectorTracker);
} else if (JsonConstants.VTYPE_STRINGARRAY.equals(transportType)) {
return decodeArray(String.class, (JsonArray) encodedJsonValue, null);
}
// Special Vaadin types
if (JsonConstants.VTYPE_CONNECTOR.equals(transportType)) {
return connectorTracker.getConnector(encodedJsonValue.asString());
}
// Legacy types
if (JsonConstants.VTYPE_STRING.equals(transportType)) {
return encodedJsonValue.asString();
} else if (JsonConstants.VTYPE_INTEGER.equals(transportType)) {
return (int) encodedJsonValue.asNumber();
} else if (JsonConstants.VTYPE_LONG.equals(transportType)) {
return (long) encodedJsonValue.asNumber();
} else if (JsonConstants.VTYPE_FLOAT.equals(transportType)) {
return (float) encodedJsonValue.asNumber();
} else if (JsonConstants.VTYPE_DOUBLE.equals(transportType)) {
return encodedJsonValue.asNumber();
} else if (JsonConstants.VTYPE_BOOLEAN.equals(transportType)) {
return encodedJsonValue.asBoolean();
}
throw new JsonException("Unknown type " + transportType);
}
private static UidlValue decodeUidlValue(JsonArray encodedJsonValue,
ConnectorTracker connectorTracker) {
String type = encodedJsonValue.getString(0);
Object decodedValue = decodeInternalType(getType(type), true,
encodedJsonValue.get(1), connectorTracker);
return new UidlValue(decodedValue);
}
private static Map