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

software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.internal.immutable;

import java.beans.Transient;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;

@SdkInternalApi
public class ImmutableIntrospector {
    private static final String BUILD_METHOD = "build";
    private static final String BUILDER_METHOD = "builder";
    private static final String TO_BUILDER_METHOD = "toBuilder";
    private static final String GET_PREFIX = "get";
    private static final String IS_PREFIX = "is";
    private static final String SET_PREFIX = "set";

    private static final String[] METHODS_TO_IGNORE = { TO_BUILDER_METHOD };

    private static volatile ImmutableIntrospector INSTANCE = null;

    // Methods from Object are commonly overridden and confuse the mapper, automatically exclude any method with a name
    // that matches a method defined on Object.
    private final Set namesToExclude;

    private ImmutableIntrospector() {
        this.namesToExclude = Collections.unmodifiableSet(
            Stream.concat(Arrays.stream(Object.class.getMethods()).map(Method::getName), Arrays.stream(METHODS_TO_IGNORE))
                  .collect(Collectors.toSet()));
    }

    public static  ImmutableInfo getImmutableInfo(Class immutableClass) {
        if (INSTANCE == null) {
            synchronized (ImmutableIntrospector.class) {
                if (INSTANCE == null) {
                    INSTANCE = new ImmutableIntrospector();
                }
            }
        }

        return INSTANCE.introspect(immutableClass);
    }

    private  ImmutableInfo introspect(Class immutableClass) {
        Class builderClass = validateAndGetBuilderClass(immutableClass);
        Optional staticBuilderMethod = findStaticBuilderMethod(immutableClass, builderClass);
        List getters = filterAndCollectGetterMethods(immutableClass.getMethods());
        Map indexedBuilderMethods = filterAndIndexBuilderMethods(builderClass.getMethods());
        Method buildMethod = extractBuildMethod(indexedBuilderMethods, immutableClass)
            .orElseThrow(
                () -> new IllegalArgumentException(
                    "An immutable builder class must have a public method named 'build()' that takes no arguments " +
                        "and returns an instance of the immutable class it builds"));

        List propertyDescriptors =
            getters.stream()
                   .map(getter -> {
                       validateGetter(getter);
                       String propertyName = normalizeGetterName(getter);

                       Method setter = extractSetterMethod(propertyName, indexedBuilderMethods, getter, builderClass)
                           .orElseThrow(
                               () -> generateExceptionForMethod(
                                   getter,
                                   "A method was found on the immutable class that does not appear to have a " +
                                       "matching setter on the builder class."));

                       return ImmutablePropertyDescriptor.create(propertyName, getter, setter);
                   }).collect(Collectors.toList());

        if (!indexedBuilderMethods.isEmpty()) {
            throw generateExceptionForMethod(indexedBuilderMethods.values().iterator().next(),
                                             "A method was found on the immutable class builder that does not appear " +
                                                 "to have a matching getter on the immutable class.");
        }

        return ImmutableInfo.builder(immutableClass)
                            .builderClass(builderClass)
                            .staticBuilderMethod(staticBuilderMethod.orElse(null))
                            .buildMethod(buildMethod)
                            .propertyDescriptors(propertyDescriptors)
                            .build();
    }

    private boolean isMappableMethod(Method method) {
        return method.getDeclaringClass() != Object.class
            && method.getAnnotation(DynamoDbIgnore.class) == null
            && method.getAnnotation(Transient.class) == null
            && !method.isSynthetic()
            && !method.isBridge()
            && !Modifier.isStatic(method.getModifiers())
            && !namesToExclude.contains(method.getName());
    }

    private Optional findStaticBuilderMethod(Class immutableClass, Class builderClass) {
        try {
            Method method = immutableClass.getMethod(BUILDER_METHOD);

            if (Modifier.isStatic(method.getModifiers()) && method.getReturnType().isAssignableFrom(builderClass)) {
                return Optional.of(method);
            }
        } catch (NoSuchMethodException ignored) {
            // no-op
        }

        return Optional.empty();
    }

    private IllegalArgumentException generateExceptionForMethod(Method getter, String message) {
        return new IllegalArgumentException(
            message + " Use the @DynamoDbIgnore annotation on the method if you do not want it to be included in the " +
                "TableSchema introspection. [Method = \"" + getter + "\"]");
    }

    private Class validateAndGetBuilderClass(Class immutableClass) {
        DynamoDbImmutable dynamoDbImmutable = immutableClass.getAnnotation(DynamoDbImmutable.class);

        if (dynamoDbImmutable == null) {
            throw new IllegalArgumentException("A DynamoDb immutable class must be annotated with @DynamoDbImmutable");
        }

        return dynamoDbImmutable.builder();
    }

    private void validateGetter(Method getter) {
        if (getter.getReturnType() == void.class || getter.getReturnType() == Void.class) {
            throw generateExceptionForMethod(getter, "A method was found on the immutable class that does not appear " +
                "to be a valid getter due to the return type being void.");
        }

        if (getter.getParameterCount() != 0) {
            throw generateExceptionForMethod(getter, "A method was found on the immutable class that does not appear " +
                "to be a valid getter due to it having one or more parameters.");
        }
    }

    private List filterAndCollectGetterMethods(Method[] rawMethods) {
        return Arrays.stream(rawMethods)
                     .filter(this::isMappableMethod)
                     .collect(Collectors.toList());
    }

    private Map filterAndIndexBuilderMethods(Method[] rawMethods) {
        return Arrays.stream(rawMethods)
                     .filter(this::isMappableMethod)
                     .collect(Collectors.toMap(this::normalizeSetterName, m -> m));
    }

    private String normalizeSetterName(Method setter) {
        String setterName = setter.getName();

        if (setterName.length() > 3
            && Character.isUpperCase(setterName.charAt(3))
            && setterName.startsWith(SET_PREFIX)) {

            return Character.toLowerCase(setterName.charAt(3)) + setterName.substring(4);
        }

        return setterName;
    }

    private String normalizeGetterName(Method getter) {
        String getterName = getter.getName();

        if (getterName.length() > 2
            && Character.isUpperCase(getterName.charAt(2))
            && getterName.startsWith(IS_PREFIX)
            && isMethodBoolean(getter)) {

            return Character.toLowerCase(getterName.charAt(2)) + getterName.substring(3);
        }

        if (getterName.length() > 3
            && Character.isUpperCase(getterName.charAt(3))
            && getterName.startsWith(GET_PREFIX)) {

            return Character.toLowerCase(getterName.charAt(3)) + getterName.substring(4);
        }

        return getterName;
    }

    private boolean isMethodBoolean(Method method) {
        return method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class;
    }

    private Optional extractBuildMethod(Map indexedBuilderMethods, Class immutableClass) {
        Method buildMethod = indexedBuilderMethods.get(BUILD_METHOD);

        if (buildMethod == null
            || buildMethod.getParameterCount() != 0
            || !immutableClass.equals(buildMethod.getReturnType())) {

            return Optional.empty();
        }

        indexedBuilderMethods.remove(BUILD_METHOD);
        return Optional.of(buildMethod);
    }

    private Optional extractSetterMethod(String propertyName,
                                                 Map indexedBuilderMethods,
                                                 Method getterMethod,
                                                 Class builderClass) {
        Method setterMethod = indexedBuilderMethods.get(propertyName);

        if (setterMethod == null
            || !setterHasValidSignature(setterMethod, getterMethod.getReturnType(), builderClass)) {
            return Optional.empty();
        }

        indexedBuilderMethods.remove(propertyName);
        return Optional.of(setterMethod);
    }

    private boolean setterHasValidSignature(Method setterMethod, Class expectedType, Class builderClass) {
        return setterHasValidParameterSignature(setterMethod, expectedType)
            && setterHasValidReturnType(setterMethod, builderClass);
    }

    private boolean setterHasValidParameterSignature(Method setterMethod, Class expectedType) {
        return setterMethod.getParameterCount() == 1 && expectedType.equals(setterMethod.getParameterTypes()[0]);
    }

    private boolean setterHasValidReturnType(Method setterMethod, Class builderClass) {
        if (setterMethod.getReturnType() == void.class || setterMethod.getReturnType() == Void.class) {
            return true;
        }

        return setterMethod.getReturnType().isAssignableFrom(builderClass);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy