diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java index 57983ba..10cc8e2 100644 --- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java @@ -19,6 +19,7 @@ */ package com.flowingcode.vaadin.jsonmigration; +import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.page.PendingJavaScriptResult; import com.vaadin.flow.dom.DomEvent; import com.vaadin.flow.dom.Element; @@ -63,6 +64,18 @@ private static Class lookup_BaseJsonNode() { } } + /** + * Converts a given Java object into the return type of a {@link ClientCallable method}. + * + * In Vaadin 25, this method converts {@code JsonValue} into {@code JsonNode}. + * + * @param object the object to convert + * @return an {@code Object} suitable to use as the result of a {@code ClientCallable} method. + */ + public static Object convertToClientCallableResult(Object object) { + return helper.convertToClientCallableResult(object); + } + /** * Converts a given Java object into a {@code JsonValue}. * diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java index 9bd05fe..aca9084 100644 --- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java @@ -29,6 +29,8 @@ interface JsonMigrationHelper { JsonValue convertToJsonValue(Object object); + Object convertToClientCallableResult(Object object); + Object invoke(Method method, Object instance, Object... args); ElementalPendingJavaScriptResult convertPendingJavaScriptResult(PendingJavaScriptResult result); diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java index c2d6d31..591b0e9 100644 --- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java @@ -55,6 +55,15 @@ public JsonValue convertToJsonValue(Object object) { } } + @Override + public Object convertToClientCallableResult(Object object) { + if (object instanceof JsonValue) { + return convertToJsonNode((JsonValue) object); + } else { + return object; + } + } + @Override @SneakyThrows public Object invoke(Method method, Object instance, Object... args) { diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java new file mode 100644 index 0000000..fbfef15 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java @@ -0,0 +1,400 @@ +/*- + * #%L + * Json Migration Helper + * %% + * Copyright (C) 2025 Flowing Code + * %% + * 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. + * #L% + */ +package com.flowingcode.vaadin.jsonmigration; + +/* + * Copyright 2000-2025 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. + */ + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonNull; +import elemental.json.JsonObject; +import elemental.json.JsonType; +import elemental.json.JsonValue; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * General-purpose serializer of Java objects to {@link JsonValue} and + * deserializer of JsonValue to Java objects. + * + * @since 1.0 + */ +public final class JsonSerializer { + + private JsonSerializer() { + } + + /** + * Converts a Java bean, String, wrapper of primitive type or enum to a {@link JsonValue}. + *

+ * When a bean is used, a {@link JsonObject} is returned. + * + * @param bean Java object to be converted + * @return the json representation of the Java object + */ + public static JsonValue toJson(Object bean) { + if (bean == null) { + return Json.createNull(); + } + if (bean instanceof Collection) { + return toJson((Collection) bean); + } + if (bean.getClass().isArray()) { + return toJsonArray(bean); + } + + Optional simpleType = tryToConvertToSimpleType(bean); + if (simpleType.isPresent()) { + return simpleType.get(); + } + + try { + JsonObject json = Json.createObject(); + Class type = bean.getClass(); + + if (type.isRecord()) { + for (RecordComponent rc : type.getRecordComponents()) { + json.put(rc.getName(), + toJson(rc.getAccessor().invoke(bean))); + } + } else { + BeanInfo info = Introspector.getBeanInfo(type); + for (PropertyDescriptor pd : info.getPropertyDescriptors()) { + if ("class".equals(pd.getName())) { + continue; + } + Method reader = pd.getReadMethod(); + if (reader != null) { + json.put(pd.getName(), toJson(reader.invoke(bean))); + } + } + } + + return json; + } catch (Exception e) { + throw new IllegalArgumentException( + "Could not serialize object of type " + bean.getClass() + + " to JsonValue", + e); + } + } + + /** + * Converts a collection of object into a {@link JsonArray}, converting each + * item of the collection individually. + * + * @param beans + * the collection of objects to be converted + * @return the json representation of the objects in the collectios. An + * empty array is returned if the input collections is + * null + */ + public static JsonArray toJson(Collection beans) { + JsonArray array = Json.createArray(); + if (beans == null) { + return array; + } + + beans.stream().map(JsonSerializer::toJson) + .forEachOrdered(json -> array.set(array.length(), json)); + return array; + } + + private static JsonArray toJsonArray(Object javaArray) { + int length = Array.getLength(javaArray); + JsonArray array = Json.createArray(); + for (int i = 0; i < length; i++) { + array.set(i, toJson(Array.get(javaArray, i))); + } + return array; + } + + private static Optional tryToConvertToSimpleType(Object bean) { + if (bean instanceof String) { + return Optional.of(Json.create((String) bean)); + } + if (bean instanceof Number) { + return Optional.of(Json.create(((Number) bean).doubleValue())); + } + if (bean instanceof Boolean) { + return Optional.of(Json.create((Boolean) bean)); + } + if (bean instanceof Character) { + return Optional.of(Json.create(Character.toString((char) bean))); + } + if (bean instanceof Enum) { + return Optional.of(Json.create(((Enum) bean).name())); + } + if (bean instanceof JsonValue) { + return Optional.of((JsonValue) bean); + } + return Optional.empty(); + } + + /** + * Converts a JsonValue to the corresponding Java object. The Java object can be a Java bean, + * String, wrapper of primitive types or an enum. + * + * @param type the type of the Java object convert the json to + * @param json the json representation of the object + * @param the resulting object type + * + * @return the deserialized object, or null if the input json is null + */ + public static T toObject(Class type, JsonValue json) { + return toObject(type, null, json); + } + + @SuppressWarnings("unchecked") + private static T toObject(Class type, Type genericType, + JsonValue json) { + if (json == null || json instanceof JsonNull) { + return null; + } + + Optional simpleType = tryToConvertFromSimpleType(type, json); + if (simpleType.isPresent()) { + return (T) simpleType.get(); + } + + if (Collection.class.isAssignableFrom(type)) { + return toCollection(type, genericType, json); + } + + if (type.isRecord()) { + return toRecord(type, json); + } + + T instance; + try { + instance = type.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException( + "Could not create an instance of type " + type + + ". Make sure it contains a default public constructor and the class is accessible.", + e); + } + + try { + + JsonObject jsonObject = (JsonObject) json; + String[] keys = jsonObject.keys(); + if (keys == null) { + return instance; + } + + BeanInfo info = Introspector.getBeanInfo(type); + Map writers = new HashMap<>(); + + for (PropertyDescriptor pd : info.getPropertyDescriptors()) { + Method writer = pd.getWriteMethod(); + if (writer != null) { + writers.put(pd.getName(), writer); + } + } + for (String key : keys) { + JsonValue jsonValue = jsonObject.get(key); + + Method method = writers.get(key); + if (method != null) { + Class parameterType = method.getParameterTypes()[0]; + Type genericParameterType = method + .getGenericParameterTypes()[0]; + Object value = toObject(parameterType, genericParameterType, + jsonValue); + method.invoke(instance, value); + } + } + + return instance; + } catch (Exception e) { + throw new IllegalArgumentException( + "Could not deserialize object of type " + type + + " from JsonValue", + e); + } + } + + private static T toRecord(Class type, JsonValue json) { + try { + RecordComponent[] components = type.getRecordComponents(); + Class[] componentTypes = new Class[components.length]; + Object[] values = new Object[components.length]; + + for (int i = 0; i < components.length; i++) { + componentTypes[i] = components[i].getType(); + values[i] = toObject(componentTypes[i], + ((JsonObject) json).get(components[i].getName())); + } + + return type.getDeclaredConstructor(componentTypes) + .newInstance(values); + } catch (Exception e) { + throw new IllegalArgumentException( + "Could not deserialize record of type " + type + + " from JsonValue", + e); + } + } + + private static T toCollection(Class type, Type genericType, + JsonValue json) { + if (json.getType() != JsonType.ARRAY) { + return null; + } + if (!(genericType instanceof ParameterizedType)) { + throw new IllegalArgumentException( + "Could not infer the generic parameterized type of the collection of class: " + + type.getName() + + ". The type is no subclass of ParameterizedType: " + + genericType); + } + JsonArray array = (JsonArray) json; + Collection collection = tryToCreateCollection(type, array.length()); + if (array.length() > 0) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Class parameterizedClass = (Class) parameterizedType + .getActualTypeArguments()[0]; + collection.addAll( + (List) toObjects(parameterizedClass, (JsonArray) json)); + } + return (T) collection; + } + + /** + * Converts a JsonArray into a collection of Java objects. The Java objects can be Java beans, + * Strings, wrappers of primitive types or enums. + * + * @param type the type of the elements in the array + * @param json the json representation of the objects + * @param the resulting objects types + * + * @return a modifiable list of converted objects. Returns an empty list if the input array is + * null + */ + public static List toObjects(Class type, JsonArray json) { + if (json == null) { + return new ArrayList<>(0); + } + List list = new ArrayList<>(json.length()); + for (int i = 0; i < json.length(); i++) { + list.add(JsonSerializer.toObject(type, json.get(i))); + } + return list; + } + + private static Optional tryToConvertFromSimpleType(Class type, + JsonValue json) { + if (type.isAssignableFrom(String.class)) { + return Optional.of(json.asString()); + } + if (type.isAssignableFrom(int.class) + || type.isAssignableFrom(Integer.class)) { + return Optional.of((int) json.asNumber()); + } + if (type.isAssignableFrom(double.class) + || type.isAssignableFrom(Double.class)) { + return Optional.of(json.asNumber()); + } + if (type.isAssignableFrom(long.class) + || type.isAssignableFrom(Long.class)) { + return Optional.of((long) json.asNumber()); + } + if (type.isAssignableFrom(short.class) + || type.isAssignableFrom(Short.class)) { + return Optional.of((short) json.asNumber()); + } + if (type.isAssignableFrom(byte.class) + || type.isAssignableFrom(Byte.class)) { + return Optional.of((byte) json.asNumber()); + } + if (type.isAssignableFrom(char.class) + || type.isAssignableFrom(Character.class)) { + return Optional.of(json.asString().charAt(0)); + } + if (type.isAssignableFrom(Boolean.class) + || type.isAssignableFrom(boolean.class)) { + return Optional.of(json.asBoolean()); + } + if (type.isEnum()) { + return Optional.of(Enum.valueOf((Class) type, + json.asString())); + } + if (JsonValue.class.isAssignableFrom(type)) { + return Optional.of(json); + } + return Optional.empty(); + + } + + private static Collection tryToCreateCollection(Class collectionType, + int initialCapacity) { + if (collectionType.isInterface()) { + if (List.class.isAssignableFrom(collectionType)) { + return new ArrayList<>(initialCapacity); + } + if (Set.class.isAssignableFrom(collectionType)) { + return new LinkedHashSet<>(initialCapacity); + } + throw new IllegalArgumentException( + "Collection type not supported: '" + + collectionType.getName() + + "'. Use Lists, Sets or concrete classes that implement java.util.Collection."); + } + try { + return (Collection) collectionType.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException( + "Could not create an instance of the collection of type " + + collectionType + + ". Make sure it contains a default public constructor and the class is accessible.", + e); + } + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java index c4b331e..a00b9bc 100644 --- a/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java @@ -39,6 +39,11 @@ public JsonValue convertToJsonValue(Object object) { object.getClass().getName() + " cannot be converted to elemental.json.JsonObject"); } } + + @Override + public Object convertToClientCallableResult(Object object) { + return object; + } @Override @SneakyThrows