From 45b64b91af8db9f056a7d8e160964edb1cc5fc4d Mon Sep 17 00:00:00 2001 From: Yordan Atanasov Date: Tue, 13 Aug 2024 23:18:16 +0300 Subject: [PATCH] feat: implement reflection for Reflectable annotated classes. Improve ObjectMapper and implement JsonSerializer. --- build.gradle.kts | 2 +- .../com/limechain/chain/spec/ChainSpec.java | 2 + .../java/com/limechain/teavm/HttpRequest.java | 3 +- .../teavm/ReflectionSupplierImpl.java | 61 +++++++ .../teavm/annotation/Reflectable.java | 26 +++ .../limechain/utils/json/JsonSerializer.java | 112 +++++++++++++ .../com/limechain/utils/json/JsonUtil.java | 4 + .../limechain/utils/json/ObjectMapper.java | 150 ++++++++---------- .../org.teavm.classlib.ReflectionSupplier | 1 + src/main/webapp/js/http.js | 2 +- 10 files changed, 271 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/limechain/teavm/ReflectionSupplierImpl.java create mode 100644 src/main/java/com/limechain/teavm/annotation/Reflectable.java create mode 100644 src/main/java/com/limechain/utils/json/JsonSerializer.java create mode 100644 src/main/resources/META-INF/services/org.teavm.classlib.ReflectionSupplier diff --git a/build.gradle.kts b/build.gradle.kts index 547195018..85e0dac4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,7 +21,7 @@ dependencies { implementation("org.projectlombok:lombok:1.18.34") implementation("org.teavm:teavm-jso-apis:0.10.0") - + implementation("org.teavm:teavm-core:0.10.0") } teavm.js { diff --git a/src/main/java/com/limechain/chain/spec/ChainSpec.java b/src/main/java/com/limechain/chain/spec/ChainSpec.java index a37a992be..36e469d04 100644 --- a/src/main/java/com/limechain/chain/spec/ChainSpec.java +++ b/src/main/java/com/limechain/chain/spec/ChainSpec.java @@ -1,5 +1,6 @@ package com.limechain.chain.spec; +import com.limechain.teavm.annotation.Reflectable; import com.limechain.utils.json.ObjectMapper; import lombok.Getter; import lombok.Setter; @@ -13,6 +14,7 @@ */ @Getter @Setter +@Reflectable public class ChainSpec implements Serializable { private String id; private String name; diff --git a/src/main/java/com/limechain/teavm/HttpRequest.java b/src/main/java/com/limechain/teavm/HttpRequest.java index 14ca815df..73684788e 100644 --- a/src/main/java/com/limechain/teavm/HttpRequest.java +++ b/src/main/java/com/limechain/teavm/HttpRequest.java @@ -1,9 +1,8 @@ package com.limechain.teavm; import org.teavm.jso.JSBody; -import org.teavm.jso.JSObject; public class HttpRequest { @JSBody(params = {"method", "url", "body"}, script = "return httpRequestSync(method, url, body);") - public static native String httpRequestSync(String method, String url, JSObject body); + public static native String httpRequestSync(String method, String url, String body); } diff --git a/src/main/java/com/limechain/teavm/ReflectionSupplierImpl.java b/src/main/java/com/limechain/teavm/ReflectionSupplierImpl.java new file mode 100644 index 000000000..8fce5ca52 --- /dev/null +++ b/src/main/java/com/limechain/teavm/ReflectionSupplierImpl.java @@ -0,0 +1,61 @@ +package com.limechain.teavm; + +import com.limechain.teavm.annotation.Reflectable; +import org.teavm.classlib.ReflectionContext; +import org.teavm.classlib.ReflectionSupplier; +import org.teavm.model.ClassReader; +import org.teavm.model.FieldReader; +import org.teavm.model.MethodDescriptor; +import org.teavm.model.MethodReader; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ReflectionSupplierImpl implements ReflectionSupplier { + @Override + public Collection getAccessibleFields(ReflectionContext context, String className) { + ClassReader cls = context.getClassSource().get(className); + if (cls == null) { + return Collections.emptyList(); + } + Set fields = new HashSet<>(); + if (cls.getAnnotations().get(Reflectable.class.getName()) != null) { + List descriptors = cls.getFields().stream() + .map(FieldReader::getName) + .toList(); + fields.addAll(descriptors); + } else { + for (FieldReader field : cls.getFields()) { + if (field.getAnnotations().get(Reflectable.class.getName()) != null) { + fields.add(field.getName()); + } + } + } + return fields; + } + + @Override + public Collection getAccessibleMethods(ReflectionContext context, String className) { + ClassReader cls = context.getClassSource().get(className); + if (cls == null) { + return Collections.emptyList(); + } + Set methods = new HashSet<>(); + if (cls.getAnnotations().get(Reflectable.class.getName()) != null) { + List descriptors = cls.getMethods().stream() + .map(MethodReader::getDescriptor) + .toList(); + methods.addAll(descriptors); + } else { + for (MethodReader method : cls.getMethods()) { + if (method.getAnnotations().get(Reflectable.class.getName()) != null) { + methods.add(method.getDescriptor()); + } + } + } + return methods; + } +} diff --git a/src/main/java/com/limechain/teavm/annotation/Reflectable.java b/src/main/java/com/limechain/teavm/annotation/Reflectable.java new file mode 100644 index 000000000..a26595318 --- /dev/null +++ b/src/main/java/com/limechain/teavm/annotation/Reflectable.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * 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.limechain.teavm.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Reflectable { +} diff --git a/src/main/java/com/limechain/utils/json/JsonSerializer.java b/src/main/java/com/limechain/utils/json/JsonSerializer.java new file mode 100644 index 000000000..86a97f926 --- /dev/null +++ b/src/main/java/com/limechain/utils/json/JsonSerializer.java @@ -0,0 +1,112 @@ +package com.limechain.utils.json; + +import com.limechain.utils.DivLogger; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +public class JsonSerializer { + + private final static DivLogger LOGGER = new DivLogger(); + + // Method to serialize any object to a JSON string + public static String serializeToJson(Object object) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{"); + + Field[] fields = object.getClass().getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + try { + Field field = fields[i]; // To access private fields + field.setAccessible(true); + String fieldName = field.getName(); + Object fieldValue = field.get(object); + + jsonBuilder.append("\"").append(fieldName).append("\":"); + appendValue(jsonBuilder, fieldValue); + + if (i < fields.length - 1) { + jsonBuilder.append(","); + } + } catch (IllegalAccessException e) { + LOGGER.log(Level.FINE, Arrays.toString(e.getStackTrace())); + } + } + + jsonBuilder.append("}"); + return jsonBuilder.toString(); + } + + // Helper method to handle different types of values + private static void appendValue(StringBuilder jsonBuilder, Object value) { + if (value == null) { + jsonBuilder.append("null"); + } else if (value instanceof String) { + jsonBuilder.append("\"").append(value).append("\""); + } else if (value instanceof Number || value instanceof Boolean) { + jsonBuilder.append(value); + } else if (value instanceof List) { + appendList(jsonBuilder, (List) value); + } else if (value instanceof Map) { + appendMap(jsonBuilder, (Map) value); + } else if (value instanceof byte[]) { + appendByteArray(jsonBuilder, (byte[]) value); + } else if (value.getClass().isArray()) { + appendArray(jsonBuilder, value); + } else { + jsonBuilder.append(serializeToJson(value)); // Recursively handle nested objects + } + } + + // Method to serialize a List to JSON + private static void appendList(StringBuilder jsonBuilder, List list) { + jsonBuilder.append("["); + for (int i = 0; i < list.size(); i++) { + appendValue(jsonBuilder, list.get(i)); + if (i < list.size() - 1) { + jsonBuilder.append(","); + } + } + jsonBuilder.append("]"); + } + + // Method to serialize a Map to JSON + private static void appendMap(StringBuilder jsonBuilder, Map map) { + jsonBuilder.append("{"); + Set keys = map.keySet(); + int i = 0; + for (Object key : keys) { + jsonBuilder.append("\"").append(key).append("\":"); + appendValue(jsonBuilder, map.get(key)); + if (i < keys.size() - 1) { + jsonBuilder.append(","); + } + i++; + } + jsonBuilder.append("}"); + } + + // Method to serialize an array to JSON + private static void appendArray(StringBuilder jsonBuilder, Object array) { + jsonBuilder.append("["); + int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + appendValue(jsonBuilder, Array.get(array, i)); + if (i < length - 1) { + jsonBuilder.append(","); + } + } + jsonBuilder.append("]"); + } + + private static void appendByteArray(StringBuilder jsonBuilder, byte[] byteArray) { + String base64 = Base64.getEncoder().encodeToString(byteArray); + jsonBuilder.append("\"").append(base64).append("\""); + } +} \ No newline at end of file diff --git a/src/main/java/com/limechain/utils/json/JsonUtil.java b/src/main/java/com/limechain/utils/json/JsonUtil.java index 8815012ba..a2a89fb3c 100644 --- a/src/main/java/com/limechain/utils/json/JsonUtil.java +++ b/src/main/java/com/limechain/utils/json/JsonUtil.java @@ -10,6 +10,10 @@ static Map parseJson(String jsonPath) { return new JsonParser(readJsonFromFile(jsonPath)).parse(); } + public String stringify(Object object) { + return JsonSerializer.serializeToJson(object); + } + private static String readJsonFromFile(String filePath) { return HttpRequest.httpRequestSync("GET", filePath, null); } diff --git a/src/main/java/com/limechain/utils/json/ObjectMapper.java b/src/main/java/com/limechain/utils/json/ObjectMapper.java index dd81ca829..c6ace1408 100644 --- a/src/main/java/com/limechain/utils/json/ObjectMapper.java +++ b/src/main/java/com/limechain/utils/json/ObjectMapper.java @@ -1,115 +1,89 @@ package com.limechain.utils.json; -import com.limechain.chain.spec.ChainSpec; +import lombok.extern.java.Log; -import java.io.IOException; -import java.util.HashMap; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.Base64; import java.util.List; import java.util.Map; +@Log public class ObjectMapper { - boolean failOnUnknown; + private final boolean failOnUnknownField; - public ObjectMapper(boolean failOnUnknown) { - this.failOnUnknown = failOnUnknown; + public ObjectMapper(boolean failOnUnknownField) { + this.failOnUnknownField = failOnUnknownField; } - public T mapToClass(String jsonPath, Class clazz) throws IOException { - Map jsonMap = JsonUtil.parseJson(jsonPath); + public T mapToClass(String jsonString, Class clazz) { + Map jsonMap = JsonUtil.parseJson(jsonString); - T instance = createInstance(clazz); - populateFields(instance, jsonMap); - - return instance; - } - - private T createInstance(Class clazz) { - if (clazz == ChainSpec.class) { - return (T) ObjectFactory.createChainSpec(); + try { + T instance = clazz.getDeclaredConstructor().newInstance(); + for (Map.Entry entry : jsonMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + Field field = findField(clazz, key); + if (field != null) { + field.setAccessible(true); + field.set(instance, convertValue(field.getType(), value)); + } + } + return instance; + } catch (Exception e) { + throw new RuntimeException("Failed to map JSON to class", e); } - // Handle other types similarly - throw new IllegalArgumentException("Unsupported class type: " + clazz.getName()); } - private void populateFields(Object instance, Map jsonMap) throws IOException { - for (Map.Entry entry : jsonMap.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - setField(instance, key, value); + private Field findField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + if (failOnUnknownField) { + throw new IllegalStateException("Field " + fieldName + " does not exist in " + clazz.getName()); + } else { + log.fine("Field " + fieldName + " does not exist in " + clazz.getName()); + return null; + } } } - private void setField(Object instance, String key, Object value) throws IOException { - if (instance instanceof ChainSpec) { - setChainSpecFields((ChainSpec) instance, key, value); - } else { - throw new IOException("Unsupported object type for field setting."); + private static Object convertValue(Class type, Object value) { + if (value == null) { + return null; } - } - private void setChainSpecFields(ChainSpec instance, String key, Object value) throws IOException { - switch (key) { - case "id": - instance.setId(convertValue(String.class, value)); - break; - case "name": - instance.setName(convertValue(String.class, value)); - break; - case "protocolId": - instance.setProtocolId(convertValue(String.class, value)); - break; - case "bootNodes": - instance.setBootNodes(convertValue(String[].class, value)); - break; - case "lightSyncState": - instance.setLightSyncState(convertValue(Map.class, value)); - break; - default: { - if (failOnUnknown) { - throw new IOException("Unsupported field key: " + key); - } + if (type.isInstance(value)) { + return value; + } else if (type == Integer.class || type == int.class) { + return ((Number) value).intValue(); + } else if (type == Double.class || type == double.class) { + return ((Number) value).doubleValue(); + } else if (type == Boolean.class || type == boolean.class || type == byte.class) { + return value; + } else if (type == String.class) { + return value.toString(); + } else if (type == byte[].class) { + if (value instanceof String) { + return Base64.getDecoder().decode((String) value); + } else { + throw new RuntimeException("Unsupported value type for byte[]: " + value.getClass()); } + } else if (type.isArray()) { + return convertArray(type.getComponentType(), (List) value); } + + // Add more type conversions as needed + throw new RuntimeException("Unsupported field type: " + type); } - private T convertValue(Class type, Object value) { - if (value == null) { - return null; - } - if (type == String.class) { - return (T) value.toString(); - } else if (type == int.class || type == Integer.class) { - return (T) Integer.valueOf(value.toString()); - } else if (type == long.class || type == Long.class) { - return (T) Long.valueOf(value.toString()); - } else if (type == double.class || type == Double.class) { - return (T) Double.valueOf(value.toString()); - } else if (type == float.class || type == Float.class) { - return (T) Float.valueOf(value.toString()); - } else if (type == boolean.class || type == Boolean.class) { - return (T) Boolean.valueOf(value.toString()); - } else if (type == char.class || type == Character.class) { - return (T) Character.valueOf(value.toString().charAt(0)); - } else if (type.isArray()) { - // Handle arrays - Class componentType = type.getComponentType(); - List list = (List) value; - Object array = java.lang.reflect.Array.newInstance(componentType, list.size()); - for (int i = 0; i < list.size(); i++) { - java.lang.reflect.Array.set(array, i, convertValue(componentType, list.get(i))); - } - return (T) array; - } else if (type == Map.class) { - // Handle maps - Map map = (Map) value; - Map resultMap = new HashMap<>(); - for (Map.Entry entry : map.entrySet()) { - resultMap.put(entry.getKey(), convertValue(String.class, entry.getValue())); - } - return (T) resultMap; - } else { - throw new IllegalArgumentException("Unsupported conversion type: " + type.getName()); + private static Object convertArray(Class componentType, List jsonArray) { + Object array = Array.newInstance(componentType, jsonArray.size()); + for (int i = 0; i < jsonArray.size(); i++) { + Array.set(array, i, convertValue(componentType, jsonArray.get(i))); } + return array; } } \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.teavm.classlib.ReflectionSupplier b/src/main/resources/META-INF/services/org.teavm.classlib.ReflectionSupplier new file mode 100644 index 000000000..b0d9ad822 --- /dev/null +++ b/src/main/resources/META-INF/services/org.teavm.classlib.ReflectionSupplier @@ -0,0 +1 @@ +com.limechain.teavm.ReflectionSupplierImpl \ No newline at end of file diff --git a/src/main/webapp/js/http.js b/src/main/webapp/js/http.js index f8dfa5716..e487222a1 100644 --- a/src/main/webapp/js/http.js +++ b/src/main/webapp/js/http.js @@ -3,7 +3,7 @@ function httpRequestSync(method, url, body) { xhr.open(method, url, false); // false for synchronous request xhr.setRequestHeader('Content-Type', 'application/json'); if (method === 'POST' && body) { - xhr.send(JSON.stringify(body)); + xhr.send(body); } else { xhr.send(); }