diff --git a/devkit-utils/src/main/java/com/onixbyte/devkit/utils/MapUtil.java b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/MapUtil.java index 01b08e1..b929cee 100644 --- a/devkit-utils/src/main/java/com/onixbyte/devkit/utils/MapUtil.java +++ b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/MapUtil.java @@ -19,204 +19,98 @@ import lombok.extern.slf4j.Slf4j; -import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; /** - * {@code MapUtil} is a utility class that provides methods for converting - * objects to maps and maps to objects. + * {@code MapUtil} is a utility class that provides methods for converting objects to maps and maps + * to objects. *

- * It also provides methods for getting and setting field values using - * reflection. + * Note: Since version 1.4.2, this util class removed reflection API and transferred to a safer API. + * Please see documentation for more information. * * @author Zihlu Wang - * @version 1.1.0 + * @version 1.4.2 + * @see com.onixbyte.devkit.utils.unsafe.ReflectMapUtil * @since 1.0.0 */ @Slf4j public final class MapUtil { /** - * Converts an object to a map by mapping the field names to their - * corresponding values. + * Converts an object to a map by mapping the field names to their corresponding values. * - * @param obj the object to be converted to a map + * @param entity the object to be converted to a map * @return a map representing the fields and their values of the object - * @throws IllegalAccessException if an error occurs while accessing the - * fields of the object */ - public static Map objectToMap(Object obj) throws IllegalAccessException { - if (obj == null) { - return null; - } - - var map = new HashMap(); - - var declaredFields = obj.getClass().getDeclaredFields(); - for (var field : declaredFields) { - field.setAccessible(true); - Object result = field.get(obj); - if (result != null) { - map.put(field.getName(), result); - } - } - - return map; + public static Map objectToMap(T entity, + Map> adapters) { + var resultMap = new HashMap(); + adapters.forEach((fieldName, adapter) -> resultMap.put(fieldName, adapter.fetch(entity))); + return resultMap; } /** - * Converts a map to an object of the specified type by setting the field - * values using the map entries. + * Converts a map to an object of the specified type by setting the field values using the + * map entries. * - * @param map the map representing the fields and their values - * @param requiredType the class of the object to be created - * @param the type of the object to be created - * @return an object of the specified type with the field values set from - * the map - * @throws NoSuchMethodException if the constructor of the required - * type is not found - * @throws InvocationTargetException if an error occurs while invoking the - * constructor - * @throws InstantiationException if the required type is abstract or an - * interface - * @throws IllegalAccessException if an error occurs while accessing the - * fields of the object + * @param objectMap the map representing the fields and their values + * @param entity an empty entity of the target class + * @param adapters the adapters to execute the setter for the entity + * @param the type of the object to be created + * @return an object of the specified type with the field values set from the map */ - public static T mapToObject(Map map, Class requiredType) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - var bean = requiredType.getConstructor().newInstance(); - if (map != null) { - for (var entry : map.entrySet()) { - try { - var entryValue = entry.getValue().toString(); - // get the field by field name - var field = requiredType.getDeclaredField(entry.getKey()); - var fieldType = field.getGenericType(); - - // convert field value by class - if (fieldType instanceof Class fieldClass) { - if (fieldClass == Short.class || fieldClass == short.class) { - entry.setValue(Short.parseShort(entryValue)); - } else if (fieldClass == Integer.class || fieldClass == int.class) { - entry.setValue(Integer.parseInt(entryValue)); - } else if (fieldClass == Long.class || fieldClass == long.class) { - entry.setValue(Long.parseLong(entryValue)); - } else if (fieldClass == Float.class || fieldClass == float.class) { - entry.setValue(Float.parseFloat(entryValue)); - } else if (fieldClass == Double.class || fieldClass == double.class) { - entry.setValue(Double.parseDouble(entryValue)); - } else if (fieldClass == Character.class || fieldClass == char.class) { - entry.setValue(entryValue.charAt(0)); - } else if (fieldClass == Byte.class || fieldClass == byte.class) { - entry.setValue(Byte.parseByte(entryValue)); - } else if (fieldClass == Boolean.class || fieldClass == boolean.class) { - entry.setValue(Boolean.parseBoolean(entryValue)); - } else if (fieldClass == String.class) { - entry.setValue(entryValue); - } else { - log.error("Unable to determine the type of property {}.", field.getName()); - continue; - } - } - - setFieldValue(bean, entry.getKey(), entry.getValue()); - } catch (Exception e) { - log.error("Map to Object failed."); - } - } - } - return bean; + public static T mapToObject(Map objectMap, + T entity, + Map> adapters) { + adapters.forEach((fieldName, adapter) -> Optional.ofNullable(objectMap) + .map((data) -> data.get(fieldName)) + .ifPresent((fieldValue) -> adapter.setValue(entity, fieldValue))); + return entity; } /** * Retrieves the value of a field from an object using reflection. * - * @param obj the object from which to retrieve the field value - * @param fieldName the name of the field - * @param fieldType the class representing the type of the field value - * @param the type of the field value - * @return the value of the field in the object, or null if the field does - * not exist or cannot be accessed - * @throws IllegalAccessException if an error occurs while accessing the - * field - * @throws InvocationTargetException if an error occurs while invoking the - * field getter method - * @throws NoSuchMethodException if the specified getter is not present + * @param the type of the field value + * @param entity the object from which to retrieve the field value + * @param adapter the adapter to execute the getter + * @return the value of the field in the object, or null if the field does not exist or cannot + * be accessed */ - public static T getFieldValue(Object obj, String fieldName, Class fieldType) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { - var methodName = getMethodName("get", fieldName); - var objectClass = obj.getClass(); - var method = objectClass.getDeclaredMethod(methodName); - - method.setAccessible(true); - return cast(method.invoke(obj), fieldType); + public static T getFieldValue(E entity, ObjectMapAdapter adapter) { + return adapter.fetch(entity); } /** * Sets the value of a field in an object using reflection. * - * @param obj the object in which to set the field value - * @param fieldName the name of the field + * @param entity the object in which to set the field value + * @param adapter the adapter to execute the setter * @param fieldValue the value to be set - * @throws InvocationTargetException if an error occurs while invoking the - * field setter method - * @throws IllegalAccessException if an error occurs while accessing the - * field - * @throws NoSuchMethodException if the specific setter is not present */ - public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { - var objectClass = obj.getClass(); - var methodName = getMethodName("set", fieldName); - var method = objectClass.getDeclaredMethod(methodName, fieldValue.getClass()); - method.setAccessible(true); - method.invoke(obj, fieldValue); + public static void setFieldValue(E entity, + ObjectMapAdapter adapter, + Object fieldValue) { + adapter.setValue(entity, fieldValue); } /** - * Casts the specified value to the required type. + * Casts the specified value to the required type with Optional. * * @param value the value to be cast * @param requiredType the type to which the value should be cast * @param the type to which the value should be cast - * @return the cast value, or null if the value cannot be cast to the - * required type + * @return the cast value, or {@code null} if the value is not an instance of the requiredType */ public static T cast(Object value, Class requiredType) { - if (requiredType.isInstance(value)) { - return requiredType.cast(value); - } - return null; - } - - /** - * Constructs a method name based on the given prefix and field name. - * - * @param prefix the prefix to be added to the field name - * @param fieldName the name of the field - * @return the constructed method name - */ - private static String getMethodName(String prefix, String fieldName) { - return prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + return Optional.ofNullable(requiredType) + .filter((clazz) -> clazz.isInstance(value)) + .map((clazz) -> clazz.cast(value)) + .orElse(null); } - /** - * Returns the default string representation of the specified object. - * - * @param obj the object for which to return the default string - * representation - * @return the default string representation of the object - */ - private static String defaultObject(Object obj) { - if (obj == null) { - return ""; - } else { - return String.valueOf(obj); - } - } - - /** - * Private constructor will protect this class from being instantiated. - */ private MapUtil() { } } diff --git a/devkit-utils/src/main/java/com/onixbyte/devkit/utils/ObjectMapAdapter.java b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/ObjectMapAdapter.java new file mode 100644 index 0000000..863aa60 --- /dev/null +++ b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/ObjectMapAdapter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024-2024 OnixByte. + * + * 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.onixbyte.devkit.utils; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Adapts an Object to a Map, making conversion between Map and Object much more safe. + * + * @param entity type + * @param field type + * @author zihluwang + * @version 1.4.2 + * @since 1.4.2 + */ +public class ObjectMapAdapter { + + private final Function getter; + + private final BiConsumer setter; + + /** + * Create an adapter. + * + * @param getter the getter of the field + * @param setter the setter of the field + */ + public ObjectMapAdapter(Function getter, BiConsumer setter) { + this.getter = getter; + this.setter = setter; + } + + /** + * Get data from the entity. + * + * @param entity the source of the data + * @return the data + */ + public T fetch(E entity) { + return getter.apply(entity); + } + + /** + * Set value to the entity. + * + * @param entity the target of the data + * @param value the value + */ + public void setValue(E entity, Object value) { + setter.accept(entity, value); + } + +} diff --git a/devkit-utils/src/main/java/com/onixbyte/devkit/utils/unsafe/ReflectMapUtil.java b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/unsafe/ReflectMapUtil.java new file mode 100644 index 0000000..c69ec69 --- /dev/null +++ b/devkit-utils/src/main/java/com/onixbyte/devkit/utils/unsafe/ReflectMapUtil.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2024 OnixByte. + * + * 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.onixbyte.devkit.utils.unsafe; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +/** + * {@code MapUtil} is a utility class that provides methods for converting + * objects to maps and maps to objects. + *

+ * It also provides methods for getting and setting field values using + * reflection. + * + * @author zihluwang + * @version 1.4.2 + * @since 1.4.2 + */ +@Slf4j +public final class ReflectMapUtil { + + /** + * Converts an object to a map by mapping the field names to their + * corresponding values. + * + * @param obj the object to be converted to a map + * @return a map representing the fields and their values of the object + * @throws IllegalAccessException if an error occurs while accessing the + * fields of the object + */ + public static Map objectToMap(Object obj) throws IllegalAccessException { + if (obj == null) { + return null; + } + + var map = new HashMap(); + + var declaredFields = obj.getClass().getDeclaredFields(); + for (var field : declaredFields) { + field.setAccessible(true); + Object result = field.get(obj); + if (result != null) { + map.put(field.getName(), result); + } + } + + return map; + } + + /** + * Converts a map to an object of the specified type by setting the field + * values using the map entries. + * + * @param map the map representing the fields and their values + * @param requiredType the class of the object to be created + * @param the type of the object to be created + * @return an object of the specified type with the field values set from + * the map + * @throws NoSuchMethodException if the constructor of the required + * type is not found + * @throws InvocationTargetException if an error occurs while invoking the + * constructor + * @throws InstantiationException if the required type is abstract or an + * interface + * @throws IllegalAccessException if an error occurs while accessing the + * fields of the object + */ + public static T mapToObject(Map map, Class requiredType) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + var bean = requiredType.getConstructor().newInstance(); + if (map != null) { + for (var entry : map.entrySet()) { + try { + var entryValue = entry.getValue().toString(); + // get the field by field name + var field = requiredType.getDeclaredField(entry.getKey()); + var fieldType = field.getGenericType(); + + // convert field value by class + if (fieldType instanceof Class fieldClass) { + if (fieldClass == Short.class || fieldClass == short.class) { + entry.setValue(Short.parseShort(entryValue)); + } else if (fieldClass == Integer.class || fieldClass == int.class) { + entry.setValue(Integer.parseInt(entryValue)); + } else if (fieldClass == Long.class || fieldClass == long.class) { + entry.setValue(Long.parseLong(entryValue)); + } else if (fieldClass == Float.class || fieldClass == float.class) { + entry.setValue(Float.parseFloat(entryValue)); + } else if (fieldClass == Double.class || fieldClass == double.class) { + entry.setValue(Double.parseDouble(entryValue)); + } else if (fieldClass == Character.class || fieldClass == char.class) { + entry.setValue(entryValue.charAt(0)); + } else if (fieldClass == Byte.class || fieldClass == byte.class) { + entry.setValue(Byte.parseByte(entryValue)); + } else if (fieldClass == Boolean.class || fieldClass == boolean.class) { + entry.setValue(Boolean.parseBoolean(entryValue)); + } else if (fieldClass == String.class) { + entry.setValue(entryValue); + } else { + log.error("Unable to determine the type of property {}.", field.getName()); + continue; + } + } + + setFieldValue(bean, entry.getKey(), entry.getValue()); + } catch (Exception e) { + log.error("Map to Object failed."); + } + } + } + return bean; + } + + /** + * Retrieves the value of a field from an object using reflection. + * + * @param obj the object from which to retrieve the field value + * @param fieldName the name of the field + * @param fieldType the class representing the type of the field value + * @param the type of the field value + * @return the value of the field in the object, or null if the field does + * not exist or cannot be accessed + * @throws IllegalAccessException if an error occurs while accessing the + * field + * @throws InvocationTargetException if an error occurs while invoking the + * field getter method + * @throws NoSuchMethodException if the specified getter is not present + */ + public static T getFieldValue(Object obj, String fieldName, Class fieldType) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + var methodName = getMethodName("get", fieldName); + var objectClass = obj.getClass(); + var method = objectClass.getDeclaredMethod(methodName); + + method.setAccessible(true); + return cast(method.invoke(obj), fieldType); + } + + /** + * Sets the value of a field in an object using reflection. + * + * @param obj the object in which to set the field value + * @param fieldName the name of the field + * @param fieldValue the value to be set + * @throws InvocationTargetException if an error occurs while invoking the + * field setter method + * @throws IllegalAccessException if an error occurs while accessing the + * field + * @throws NoSuchMethodException if the specific setter is not present + */ + public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + var objectClass = obj.getClass(); + var methodName = getMethodName("set", fieldName); + var method = objectClass.getDeclaredMethod(methodName, fieldValue.getClass()); + method.setAccessible(true); + method.invoke(obj, fieldValue); + } + + /** + * Casts the specified value to the required type. + * + * @param value the value to be cast + * @param requiredType the type to which the value should be cast + * @param the type to which the value should be cast + * @return the cast value, or null if the value cannot be cast to the + * required type + */ + public static T cast(Object value, Class requiredType) { + if (requiredType.isInstance(value)) { + return requiredType.cast(value); + } + return null; + } + + /** + * Constructs a method name based on the given prefix and field name. + * + * @param prefix the prefix to be added to the field name + * @param fieldName the name of the field + * @return the constructed method name + */ + private static String getMethodName(String prefix, String fieldName) { + return prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + } + + /** + * Returns the default string representation of the specified object. + * + * @param obj the object for which to return the default string + * representation + * @return the default string representation of the object + */ + private static String defaultObject(Object obj) { + if (obj == null) { + return ""; + } else { + return String.valueOf(obj); + } + } + + /** + * Private constructor will protect this class from being instantiated. + */ + private ReflectMapUtil() { + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b1df34e..1e3dd3e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,4 +27,5 @@ include( "simple-jwt-jjwt", "simple-jwt-spring-boot-starter", "property-guard-spring-boot-starter" -) \ No newline at end of file +) +include("map-util-unsafe")