All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.httprpc.kilo.beans.BeanAdapter Maven / Gradle / Ivy

There is a newer version: 4.9
Show newest version
/*
 * 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 org.httprpc.kilo.beans;

import org.httprpc.kilo.Name;
import org.httprpc.kilo.Required;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAmount;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static org.httprpc.kilo.util.Optionals.*;

/**
 * Provides access to Java bean properties via the {@link Map} interface.
 */
public class BeanAdapter extends AbstractMap {
    /**
     * Represents a bean property or record component.
     */
    public static class Property {
        private Method accessor = null;
        private Method mutator = null;

        private Property() {
        }

        /**
         * Returns the property's accessor.
         *
         * @return
         * The property's accessor.
         */
        public Method getAccessor() {
            return accessor;
        }

        /**
         * Returns the property's mutator.
         *
         * @return
         * The property's mutator, or {@code null} if no mutator is defined.
         */
        public Method getMutator() {
            return mutator;
        }
    }

    // Array adapter
    private static class ArrayAdapter extends AbstractList {
        Object array;
        int length;

        ArrayAdapter(Object array) {
            this.array = array;

            length = Array.getLength(array);
        }

        @Override
        public Object get(int index) {
            return adapt(Array.get(array, index));
        }

        @Override
        public int size() {
            return length;
        }

        @Override
        public Iterator iterator() {
            return new Iterator<>() {
                int i = 0;

                @Override
                public boolean hasNext() {
                    return i < length;
                }

                @Override
                public Object next() {
                    return adapt(get(i++));
                }
            };
        }
    }

    // Iterable adapter
    private static class IterableAdapter extends AbstractList {
        Iterable iterable;

        IterableAdapter(Iterable iterable) {
            this.iterable = iterable;
        }

        @Override
        public Object get(int index) {
            if (iterable instanceof List list) {
                return adapt(list.get(index));
            } else {
                throw new UnsupportedOperationException("Iterable is not a list.");
            }
        }

        @Override
        public int size() {
            if (iterable instanceof Collection collection) {
                return collection.size();
            } else {
                throw new UnsupportedOperationException("Iterable is not a collection.");
            }
        }

        @Override
        public Iterator iterator() {
            return new Iterator<>() {
                Iterator iterator = iterable.iterator();

                @Override
                public boolean hasNext() {
                    return iterator.hasNext();
                }

                @Override
                public Object next() {
                    return adapt(iterator.next());
                }
            };
        }
    }

    // Map adapter
    private static class MapAdapter extends AbstractMap {
        Map map;

        MapAdapter(Map map) {
            this.map = map;
        }

        @Override
        public Object get(Object key) {
            return adapt(map.get(key));
        }

        @Override
        public Set> entrySet() {
            return new AbstractSet<>() {
                @Override
                public int size() {
                    return map.size();
                }

                @Override
                public Iterator> iterator() {
                    return new Iterator<>() {
                        Iterator> iterator = map.entrySet().iterator();

                        @Override
                        public boolean hasNext() {
                            return iterator.hasNext();
                        }

                        @Override
                        public Entry next() {
                            return new Entry<>() {
                                Entry entry = iterator.next();

                                @Override
                                public Object getKey() {
                                    return entry.getKey();
                                }

                                @Override
                                public Object getValue() {
                                    return adapt(entry.getValue());
                                }

                                @Override
                                public Object setValue(Object value) {
                                    throw new UnsupportedOperationException();
                                }
                            };
                        }
                    };
                }
            };
        }
    }

    // Record adapter
    private static class RecordAdapter extends AbstractMap {
        Object value;

        Map properties;

        RecordAdapter(Object value) {
            this.value = value;

            properties = getProperties(value.getClass());
        }

        @Override
        public Object get(Object key) {
            var property = properties.get(key);

            if (property == null) {
                return null;
            }

            try {
                return adapt(property.accessor.invoke(value));
            } catch (IllegalAccessException | InvocationTargetException exception) {
                throw new RuntimeException(exception);
            }
        }

        @Override
        public Set> entrySet() {
            return new AbstractSet<>() {
                @Override
                public int size() {
                    return properties.size();
                }

                @Override
                public Iterator> iterator() {
                    return new Iterator<>() {
                        Iterator> iterator = properties.entrySet().iterator();

                        @Override
                        public boolean hasNext() {
                            return iterator.hasNext();
                        }

                        @Override
                        public Entry next() {
                            var entry = iterator.next();

                            var key = entry.getKey();

                            try {
                                var property = entry.getValue();

                                return new SimpleImmutableEntry<>(key, adapt(property.accessor.invoke(value)));
                            } catch (IllegalAccessException | InvocationTargetException exception) {
                                throw new RuntimeException(exception);
                            }
                        }
                    };
                }
            };
        }
    }

    // Typed invocation handler
    private static class TypedInvocationHandler implements InvocationHandler {
        Map map;
        Class type;

        Map accessors = new HashMap<>();

        TypedInvocationHandler(Map map, Class type) {
            this.map = map;
            this.type = type;

            var methods = type.getMethods();

            for (var i = 0; i < methods.length; i++) {
                var method = methods[i];

                if (method.getDeclaringClass() == Object.class) {
                    continue;
                }

                var propertyName = getPropertyName(method);

                if (propertyName == null) {
                    continue;
                }

                if (method.getParameterCount() == 0) {
                    accessors.put(propertyName, method);
                }
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
            if (method.getDeclaringClass() == Object.class) {
                try {
                    return method.invoke(this, arguments);
                } catch (IllegalAccessException | InvocationTargetException exception) {
                    throw new RuntimeException(exception);
                }
            } else if (method.isDefault()) {
                return InvocationHandler.invokeDefault(proxy, method, arguments);
            } else {
                var propertyName = getPropertyName(method);

                if (propertyName == null) {
                    throw new UnsupportedOperationException("Invalid method.");
                }

                if (method.getParameterCount() == 0) {
                    var key = getKey(method, propertyName);

                    var value = map.get(key);

                    if (method.getAnnotation(Required.class) != null && value == null) {
                        throw new UnsupportedOperationException("Value is not defined.");
                    }

                    return toGenericType(value, method.getGenericReturnType());
                } else {
                    var accessor = accessors.get(propertyName);

                    if (accessor == null) {
                        throw new UnsupportedOperationException("Missing accessor.");
                    }

                    var key = getKey(accessor, propertyName);

                    var value = arguments[0];

                    if (accessor.getAnnotation(Required.class) != null && value == null) {
                        throw new IllegalArgumentException("Value is required.");
                    }

                    ((Map)map).put(key, adapt(value));

                    return null;
                }
            }
        }

        @Override
        public int hashCode() {
            return 0;
        }

        @Override
        public boolean equals(Object object) {
            if (object instanceof Proxy
                && Proxy.getInvocationHandler(object) instanceof TypedInvocationHandler typedInvocationHandler
                && type == typedInvocationHandler.type) {
                for (var entry : accessors.entrySet()) {
                    var propertyName = entry.getKey();
                    var accessor = entry.getValue();

                    var key = getKey(accessor, propertyName);

                    var type = accessor.getGenericReturnType();

                    var value1 = toGenericType(map.get(key), type);
                    var value2 = toGenericType(typedInvocationHandler.map.get(key), type);

                    if (!Objects.equals(value1, value2)) {
                        return false;
                    }
                }

                return true;
            } else {
                return false;
            }
        }

        @Override
        public String toString() {
            return map.toString();
        }
    }

    // Container type
    private static class ContainerType implements ParameterizedType {
        Type[] actualTypeArguments;
        Type rawType;

        ContainerType(Type[] actualTypeArguments, Type rawType) {
            this.actualTypeArguments = actualTypeArguments;
            this.rawType = rawType;
        }

        @Override
        public Type[] getActualTypeArguments() {
            return actualTypeArguments;
        }

        @Override
        public Type getRawType() {
            return rawType;
        }

        @Override
        public Type getOwnerType() {
            return null;
        }
    }

    private Object bean;

    private Map properties;

    private static final String GET_PREFIX = "get";
    private static final String IS_PREFIX = "is";

    private static final String SET_PREFIX = "set";

    private static Map, Map> typeProperties = new ConcurrentHashMap<>();

    /**
     * Constructs a new bean adapter.
     *
     * @param bean
     * The bean instance.
     */
    public BeanAdapter(Object bean) {
        if (bean == null) {
            throw new IllegalArgumentException();
        }

        this.bean = bean;

        var type = bean.getClass();

        if (Proxy.class.isAssignableFrom(type)) {
            var interfaces = type.getInterfaces();

            if (interfaces.length == 0) {
                throw new UnsupportedOperationException("Type does not implement any interfaces.");
            }

            type = interfaces[0];
        }

        properties = getProperties(type);
    }

    /**
     * Gets a property value.
     * {@inheritDoc}
     */
    @Override
    public Object get(Object key) {
        if (key == null) {
            throw new IllegalArgumentException();
        }

        var property = properties.get(key);

        if (property == null) {
            return null;
        }

        Object value;
        try {
            value = property.accessor.invoke(bean);
        } catch (IllegalAccessException | InvocationTargetException exception) {
            throw new RuntimeException(exception);
        }

        if (property.accessor.getAnnotation(Required.class) != null && value == null) {
            throw new UnsupportedOperationException("Value is not defined.");
        }

        return adapt(value);
    }

    /**
     * Sets a property value.
     * {@inheritDoc}
     */
    @Override
    public Object put(String key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException();
        }

        var property = properties.get(key);

        if (property == null || property.mutator == null) {
            throw new UnsupportedOperationException("Property is not defined or is not writable.");
        }

        if (property.accessor.getAnnotation(Required.class) != null && value == null) {
            throw new IllegalArgumentException("Value is required.");
        }

        try {
            property.mutator.invoke(bean, toGenericType(value, property.mutator.getGenericParameterTypes()[0]));
        } catch (IllegalAccessException | InvocationTargetException exception) {
            throw new RuntimeException(exception);
        }

        return null;
    }

    /**
     * Enumerates property values.
     * {@inheritDoc}
     */
    @Override
    public Set> entrySet() {
        return new AbstractSet<>() {
            @Override
            public int size() {
                return properties.size();
            }

            @Override
            public Iterator> iterator() {
                return new Iterator<>() {
                    Iterator> iterator = properties.entrySet().iterator();

                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    @Override
                    public Entry next() {
                        var entry = iterator.next();

                        var key = entry.getKey();

                        try {
                            var property = entry.getValue();

                            var value = property.accessor.invoke(bean);

                            if (property.accessor.getAnnotation(Required.class) != null && value == null) {
                                throw new UnsupportedOperationException("Required value is not defined.");
                            }

                            return new SimpleImmutableEntry<>(key, adapt(value));
                        } catch (IllegalAccessException | InvocationTargetException exception) {
                            throw new RuntimeException(exception);
                        }
                    }
                };
            }
        };
    }

    /**
     * 

Adapts a value for loose typing. If the value is {@code null} or an * instance of one of the following types, it is returned as is:

* *
    *
  • {@link Number}
  • *
  • {@link Boolean}
  • *
  • {@link String}
  • *
  • {@link Character}
  • *
  • {@link Enum}
  • *
  • {@link Date}
  • *
  • {@link TemporalAccessor}
  • *
  • {@link TemporalAmount}
  • *
  • {@link UUID}
  • *
* *

If the value is an array, it is wrapped in a {@link List} that will * recursively adapt the array's elements.

* *

If the value is an {@link Iterable}, it is wrapped in a {@link List} * that will recursively adapt the iterable's elements. If the iterable * implements {@link Collection}, the adapter will support the * {@link Collection#size()} method. If the iterable implements * {@link List}, the adapter will support the {@link List#get(int)} * method.

* *

If the value is a {@link Map}, it is wrapped in a {@link Map} that * will recursively adapt the map's values. Map keys are not adapted.

* *

If the value is a {@link Record}, it is wrapped in a {@link Map} that * will recursively adapt the record's fields.

* *

If none of the previous conditions apply, the value is assumed to be * a bean and is wrapped in a {@link BeanAdapter}.

* * @param value * The value to adapt. * * @return * The adapted value. */ public static Object adapt(Object value) { if (value == null || value instanceof Number || value instanceof Boolean || value instanceof String || value instanceof Character || value instanceof Enum || value instanceof Date || value instanceof TemporalAccessor || value instanceof TemporalAmount || value instanceof UUID) { return value; } else if (value.getClass().isArray()) { return new ArrayAdapter(value); } else if (value instanceof Iterable iterable) { return new IterableAdapter(iterable); } else if (value instanceof Map map) { return new MapAdapter(map); } else if (value instanceof Record) { return new RecordAdapter(value); } else { return new BeanAdapter(value); } } /** *

Coerces a value to a given type. If the value is already an instance * of the target type, it is returned as is. Otherwise, if the type is one * of the following, the return value is obtained via an appropriate * conversion method:

* *
    *
  • {@link Byte} or {@code byte}
  • *
  • {@link Short} or {@code short}
  • *
  • {@link Integer} or {@code int}
  • *
  • {@link Long} or {@code long}
  • *
  • {@link Float} or {@code float}
  • *
  • {@link Double} or {@code double}
  • *
  • {@link Boolean} or {@code boolean}
  • *
  • {@link Character} or {@code char}
  • *
  • {@link String}
  • *
  • {@link Date}
  • *
  • {@link Instant}
  • *
  • {@link LocalDate}
  • *
  • {@link LocalTime}
  • *
  • {@link LocalDateTime}
  • *
  • {@link Duration}
  • *
  • {@link Period}
  • *
  • {@link UUID}
  • *
* *

If the target type is an array, the provided value must be an array * or {@link Collection}. The return value is an array of the same length * as the provided value whose elements have been coerced to the array's * component type.

* *

If the target type is an {@link Enum}, the resulting value is the * first constant whose string representation matches the value's string * representation.

* *

If none of the previous conditions apply, the provided value is * assumed to be a map. If the if the target type is a {@link Record}, the * resulting value is instantiated via the type's canonical constructor * using the entries in the map. Otherwise, the target type is assumed to * be a bean:

* *
    *
  • If the type is an interface, the return value is a proxy that maps * accessor and mutator methods to entries in the map. The proxy implements * {@link Object#equals(Object)} and delegates {@link Object#toString()} to * the map.
  • *
  • If the type is a concrete class, an instance of the type is * dynamically created and populated using the entries in the map.
  • *
* *

For reference types, {@code null} values are returned as is. For * numeric, boolean, or character primitives, they are converted to 0, * {@code false}, or the null character, respectively.

* * @param * The target type. * * @param value * The value to coerce. * * @param type * The target type. * * @return * The coerced value. */ @SuppressWarnings("unchecked") public static T coerce(Object value, Class type) { return (T)toGenericType(value, type); } /** * Coerces a collection to a list. * * @param * The target element type. * * @param collection * The collection to coerce. * * @param elementType * The target element type. * * @return * A list containing the coerced elements. */ @SuppressWarnings("unchecked") public static List coerceList(Collection collection, Class elementType) { return (List)toGenericType(collection, new ContainerType(new Type[] {elementType}, List.class)); } /** * Coerces map values. * * @param * The key type. * * @param * The target value type. * * @param map * The map to coerce. * * @param valueType * The target value type. * * @return * A map containing the coerced values. */ @SuppressWarnings("unchecked") public static Map coerceMap(Map map, Class valueType) { return (Map)toGenericType(map, new ContainerType(new Type[] {Object.class, valueType}, Map.class)); } /** * Coerces a collection to a set. * * @param * The target element type. * * @param collection * The collection to coerce. * * @param elementType * The target element type. * * @return * A set containing the coerced elements. */ @SuppressWarnings("unchecked") public static Set coerceSet(Collection collection, Class elementType) { return (Set)toGenericType(collection, new ContainerType(new Type[] {elementType}, Set.class)); } /** * Converts a value to a generic type. * * @param value * The value to convert. * * @param type * The target type. * * @return * The converted value. */ public static Object toGenericType(Object value, Type type) { if (type instanceof Class rawType) { return toRawType(value, rawType); } else if (type instanceof ParameterizedType parameterizedType) { var rawType = parameterizedType.getRawType(); var actualTypeArguments = parameterizedType.getActualTypeArguments(); if (rawType == List.class) { if (value == null) { return null; } else if (value instanceof Collection collection) { var elementType = actualTypeArguments[0]; var genericList = new ArrayList<>(collection.size()); for (var element : collection) { genericList.add(toGenericType(element, elementType)); } return genericList; } else { throw new IllegalArgumentException("Value is not a collection."); } } else if (rawType == Map.class) { if (value == null) { return null; } else if (value instanceof Map map) { var keyType = actualTypeArguments[0]; var valueType = actualTypeArguments[1]; var genericMap = new LinkedHashMap<>(); for (var entry : map.entrySet()) { genericMap.put(toGenericType(entry.getKey(), keyType), toGenericType(entry.getValue(), valueType)); } return genericMap; } else { throw new IllegalArgumentException("Value is not a map."); } } else if (rawType == Set.class) { if (value == null) { return null; } else if (value instanceof Collection collection) { var elementType = actualTypeArguments[0]; var genericSet = new LinkedHashSet<>(collection.size()); for (var element : collection) { genericSet.add(toGenericType(element, elementType)); } return genericSet; } else { throw new IllegalArgumentException("Value is not a collection."); } } else { throw new UnsupportedOperationException("Unsupported parameterized type."); } } else { throw new UnsupportedOperationException("Unsupported type."); } } private static Object toRawType(Object value, Class type) { if (type.isInstance(value)) { return value; } else if (type == Byte.TYPE || type == Byte.class) { if (value == null) { return (type == Byte.TYPE) ? (byte)0 : null; } else if (value instanceof Number number) { return number.byteValue(); } else { return Byte.parseByte(value.toString()); } } else if (type == Short.TYPE || type == Short.class) { if (value == null) { return (type == Short.TYPE) ? (short)0 : null; } else if (value instanceof Number number) { return number.shortValue(); } else { return Short.parseShort(value.toString()); } } else if (type == Integer.TYPE || type == Integer.class) { if (value == null) { return (type == Integer.TYPE) ? 0 : null; } else if (value instanceof Number number) { return number.intValue(); } else { return Integer.parseInt(value.toString()); } } else if (type == Long.TYPE || type == Long.class) { if (value == null) { return (type == Long.TYPE) ? 0L : null; } else if (value instanceof Number number) { return number.longValue(); } else { return Long.parseLong(value.toString()); } } else if (type == Float.TYPE || type == Float.class) { if (value == null) { return (type == Float.TYPE) ? 0.0f : null; } else if (value instanceof Number number) { return number.floatValue(); } else { return Float.parseFloat(value.toString()); } } else if (type == Double.TYPE || type == Double.class) { if (value == null) { return (type == Double.TYPE) ? 0.0 : null; } else if (value instanceof Number number) { return number.doubleValue(); } else { return Double.parseDouble(value.toString()); } } else if (type == Boolean.TYPE || type == Boolean.class) { if (value == null) { return (type == Boolean.TYPE) ? Boolean.FALSE : null; } else if (value instanceof Number) { return Double.compare(((Number)value).doubleValue(), 0.0) != 0; } else { return Boolean.parseBoolean(value.toString()); } } else if (type == Character.class || type == Character.TYPE) { if (value == null) { return '\0'; } else { return value.toString().charAt(0); } } else { if (value == null) { return null; } if (type == String.class) { return value.toString(); } else if (type == Date.class) { if (value instanceof Number number) { return new Date(number.longValue()); } else { return new Date(Long.parseLong(value.toString())); } } else if (type == Instant.class) { if (value instanceof Date date) { return date.toInstant(); } else { return Instant.parse(value.toString()); } } else if (type == LocalDate.class) { return LocalDate.parse(value.toString()); } else if (type == LocalTime.class) { return LocalTime.parse(value.toString()); } else if (type == LocalDateTime.class) { return LocalDateTime.parse(value.toString()); } else if (type == Duration.class) { if (value instanceof Number number) { return Duration.ofMillis(number.longValue()); } else { return Duration.parse(value.toString()); } } else if (type == Period.class) { return Period.parse(value.toString()); } else if (type == UUID.class) { return UUID.fromString(value.toString()); } else if (type.isArray()) { if (value.getClass().isArray()) { return toArray(new ArrayAdapter(value), type); } else if (value instanceof Collection collection) { return toArray(collection, type); } else { throw new IllegalArgumentException("Value is not an array or collection."); } } else if (type.isEnum()) { return toEnum(value.toString(), type); } else { if (value instanceof Map map) { if (type.isRecord()) { return toRecord(map, type); } else { return toBean(map, type); } } else { throw new IllegalArgumentException("Value is not a map."); } } } } private static Object toArray(Collection collection, Class type) { var componentType = type.getComponentType(); var array = Array.newInstance(componentType, collection.size()); var i = 0; for (var element : collection) { Array.set(array, i++, toRawType(element, componentType)); } return array; } private static Object toEnum(String value, Class type) { var fields = type.getDeclaredFields(); for (var i = 0; i < fields.length; i++) { var field = fields[i]; if (!field.isEnumConstant()) { continue; } Object constant; try { constant = field.get(null); } catch (IllegalAccessException exception) { throw new RuntimeException(exception); } if (value.equals(constant.toString())) { return constant; } } throw new IllegalArgumentException("Invalid value."); } private static Object toRecord(Map map, Class type) { var recordComponents = type.getRecordComponents(); var parameterTypes = new Class[recordComponents.length]; var arguments = new Object[recordComponents.length]; for (var i = 0; i < recordComponents.length; i++) { var recordComponent = recordComponents[i]; parameterTypes[i] = recordComponent.getType(); var name = recordComponent.getName(); var accessor = recordComponent.getAccessor(); var genericType = recordComponent.getGenericType(); var value = map.get(getKey(accessor, name)); if (accessor.getAnnotation(Required.class) != null && value == null) { throw new IllegalArgumentException("Required value is not defined."); } arguments[i] = toGenericType(value, genericType); } Constructor constructor; try { constructor = type.getDeclaredConstructor(parameterTypes); } catch (NoSuchMethodException exception) { throw new RuntimeException(exception); } try { return constructor.newInstance(arguments); } catch (InstantiationException | IllegalAccessException | InvocationTargetException exception) { throw new RuntimeException(exception); } } private static Object toBean(Map map, Class type) { if (type.isInterface()) { return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, new TypedInvocationHandler(map, type))); } else { Constructor constructor; try { constructor = type.getConstructor(); } catch (NoSuchMethodException exception) { throw new RuntimeException(exception); } Object bean; try { bean = constructor.newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException exception) { throw new RuntimeException(exception); } for (var entry : getProperties(type).entrySet()) { var property = entry.getValue(); if (property.mutator == null) { continue; } var value = map.get(entry.getKey()); if (property.accessor.getAnnotation(Required.class) != null && value == null) { throw new IllegalArgumentException("Required value is not defined."); } try { property.mutator.invoke(bean, toGenericType(value, property.mutator.getGenericParameterTypes()[0])); } catch (IllegalAccessException | InvocationTargetException exception) { throw new RuntimeException(exception); } } return bean; } } /** * Returns the properties for a given type, sorted by name. * * @param type * The bean type. * * @return * The properties defined by the requested type. */ public static Map getProperties(Class type) { return typeProperties.computeIfAbsent(type, BeanAdapter::computeProperties); } private static Map computeProperties(Class type) { var properties = new HashMap(); if (type.isRecord()) { var recordComponents = type.getRecordComponents(); for (var i = 0; i < recordComponents.length; i++) { var recordComponent = recordComponents[i]; var property = new Property(); property.accessor = recordComponent.getAccessor(); properties.put(recordComponent.getName(), property); } } else { var methods = type.getMethods(); for (var i = 0; i < methods.length; i++) { var method = methods[i]; if (method.getDeclaringClass() == Object.class) { continue; } var propertyName = getPropertyName(method); if (propertyName == null) { continue; } var property = properties.computeIfAbsent(propertyName, key -> new Property()); if (method.getParameterCount() == 0) { property.accessor = method; } else if (property.mutator == null) { property.mutator = method; } else { throw new UnsupportedOperationException("Duplicate mutator."); } } } return properties.entrySet().stream().peek(entry -> { var value = entry.getValue(); var accessor = value.getAccessor(); if (accessor == null) { throw new UnsupportedOperationException("Missing accessor."); } var mutator = value.getMutator(); if (mutator != null && !accessor.getGenericReturnType().equals(mutator.getGenericParameterTypes()[0])) { throw new UnsupportedOperationException("Property type mismatch."); } }).collect(Collectors.toMap(entry -> { var accessor = entry.getValue().getAccessor(); return getKey(accessor, entry.getKey()); }, Map.Entry::getValue, (v1, v2) -> { throw new UnsupportedOperationException("Duplicate name."); }, TreeMap::new)); } private static String getPropertyName(Method method) { if (method.isBridge()) { return null; } var methodName = method.getName(); var returnType = method.getReturnType(); var parameterCount = method.getParameterCount(); String prefix; if (methodName.startsWith(GET_PREFIX) && !(returnType == Void.TYPE || returnType == Void.class) && parameterCount == 0) { prefix = GET_PREFIX; } else if (methodName.startsWith(IS_PREFIX) && (returnType == Boolean.TYPE || returnType == Boolean.class) && parameterCount == 0) { prefix = IS_PREFIX; } else if (methodName.startsWith(SET_PREFIX) && (returnType == Void.TYPE || returnType == Void.class) && parameterCount == 1) { prefix = SET_PREFIX; } else { prefix = null; } if (prefix == null) { return null; } var j = prefix.length(); var n = methodName.length(); if (j == n) { return null; } var c = methodName.charAt(j++); if (j == n || Character.isLowerCase(methodName.charAt(j))) { c = Character.toLowerCase(c); } return c + methodName.substring(j); } private static String getKey(Method accessor, String propertyName) { return coalesce(map(accessor.getAnnotation(Name.class), Name::value), propertyName); } }