com.vaadin.server.JsonCodec Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* 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.Locale;
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 com.vaadin.util.ReflectTools;
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 final ConcurrentMap, Collection> TYPE_PROPERTY_CACHE = new ConcurrentHashMap<>();
private static final Map, String> TYPE_TO_TRANSPORT_TYPE = new HashMap<>();
/**
* Note! This does not contain primitives.
*
*/
private static final Map> TRANSPORT_TYPE_TO_TYPE = new HashMap<>();
private static final Map, JSONSerializer>> CUSTOM_SERIALIZERS = new HashMap<>();
static {
CUSTOM_SERIALIZERS.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) {
TYPE_TO_TRANSPORT_TYPE.put(type, transportType);
if (!type.isPrimitive()) {
TRANSPORT_TYPE_TO_TYPE.put(transportType, type);
}
}
public static boolean isInternalTransportType(String transportType) {
return TRANSPORT_TYPE_TO_TYPE.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 TYPE_TO_TRANSPORT_TYPE.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 TRANSPORT_TYPE_TO_TYPE.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 (CUSTOM_SERIALIZERS
.containsKey(getClassForType(targetType))) {
return CUSTOM_SERIALIZERS.get(getClassForType(targetType))
.deserialize(targetType, value, connectorTracker);
} else if (Enum.class.isAssignableFrom(getClassForType(targetType))) {
Class> classForType = getClassForType(targetType);
return decodeEnum(classForType.asSubclass(Enum.class),
(JsonString) value);
} 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 restrictToInternalTypes
* @param encodedJsonValue
* @param connectorTracker
* @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((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);
}
/**
* Set a custom JSONSerializer for a specific Class. Existence of custom
* serializers is checked after basic types (Strings, Booleans, Numbers,
* Characters), Collections and Maps, so setting custom serializers for
* these won't have any effect.
*
* To remove a previously set serializer, call this method with the second
* parameter set to {@code null}.
*
* Custom serializers should only be added from static initializers or other
* places that are guaranteed to run only once. Trying to add a serializer
* to a class that already has one will cause an exception.
*
* Warning: removing existing custom serializers may lead into unexpected
* behavior in components that expect the customized data. The framework's
* custom serializers are loaded in the static initializer block of this
* class.
*
* @see DateSerializer
* @throws IllegalArgumentException
* Thrown if parameter clazz is null.
* @throws IllegalStateException
* Thrown if serializer for parameter clazz is already
* registered and parameter jsonSerializer is not null.
* @param clazz
* The target class.
* @param jsonSerializer
* Custom JSONSerializer to add. If {@code null}, remove custom
* serializer from class clazz.
*/
public static void setCustomSerializer(Class clazz,
JSONSerializer jsonSerializer) {
if (clazz == null) {
throw new IllegalArgumentException(
"Cannot add serializer for null");
}
if (jsonSerializer == null) {
CUSTOM_SERIALIZERS.remove(clazz);
} else {
if (CUSTOM_SERIALIZERS.containsKey(clazz)) {
String err = String.format(
"Class %s already has a custom serializer. "
+ "This exception can be thrown if you try to "
+ "add a serializer from a non-static context. "
+ "Try using a static block instead.",
clazz.getName());
throw new IllegalStateException(err);
}
CUSTOM_SERIALIZERS.put(clazz, jsonSerializer);
}
}
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