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

software.amazon.awssdk.codegen.poet.model.MemberCopierSpec 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.codegen.poet.model;

import static software.amazon.awssdk.codegen.poet.model.TypeProvider.ShapeTransformation.NONE;
import static software.amazon.awssdk.codegen.poet.model.TypeProvider.ShapeTransformation.USE_BUILDER;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import software.amazon.awssdk.codegen.internal.Utils;
import software.amazon.awssdk.codegen.model.intermediate.MapModel;
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
import software.amazon.awssdk.codegen.poet.ClassSpec;
import software.amazon.awssdk.codegen.poet.PoetExtension;
import software.amazon.awssdk.codegen.poet.PoetUtils;
import software.amazon.awssdk.codegen.poet.StaticImport;
import software.amazon.awssdk.codegen.poet.model.TypeProvider.TypeNameOptions;
import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList;
import software.amazon.awssdk.core.util.DefaultSdkAutoConstructMap;
import software.amazon.awssdk.core.util.SdkAutoConstructList;
import software.amazon.awssdk.core.util.SdkAutoConstructMap;

class MemberCopierSpec implements ClassSpec {
    private final MemberModel memberModel;
    private final ServiceModelCopiers serviceModelCopiers;
    private final TypeProvider typeProvider;
    private final PoetExtension poetExtensions;

    private enum EnumTransform {
        /** Copy enums as strings */
        STRING_TO_ENUM,
        /** Copy strings as enums */
        ENUM_TO_STRING,
        /** Copy without a transformation */
        NONE
    }

    private enum BuilderTransform {
        BUILDER_TO_BUILDABLE,
        BUILDABLE_TO_BUILDER,
        NONE
    }

    MemberCopierSpec(MemberModel memberModel,
                     ServiceModelCopiers serviceModelCopiers,
                     TypeProvider typeProvider,
                     PoetExtension poetExtensions) {
        this.memberModel = memberModel;
        this.serviceModelCopiers = serviceModelCopiers;
        this.typeProvider = typeProvider;
        this.poetExtensions = poetExtensions;
    }

    @Override
    public TypeSpec poetSpec() {
        TypeSpec.Builder builder = TypeSpec.classBuilder(className())
                .addModifiers(Modifier.FINAL)
                .addAnnotation(PoetUtils.generatedAnnotation())
                .addMethod(copyMethod());

        if (memberModel.containsBuildable()) {
            builder.addMethod(copyFromBuilderMethod());
            builder.addMethod(copyToBuilderMethod());
        }

        // If this is a collection, and it contains enums, or recursively
        // contains enums, add extra methods for copying the elements from an
        // enum to string and vice versa
        if (isEnumCopyAvailable(memberModel)) {
            builder.addMethod(enumToStringCopyMethod());
            builder.addMethod(stringToEnumCopyMethod());
        }

        return builder.build();
    }

    @Override
    public ClassName className() {
        return serviceModelCopiers.copierClassFor(memberModel).get();
    }

    @Override
    public Iterable staticImports() {
        if (memberModel.isList()) {
            return Collections.singletonList(StaticImport.staticMethodImport(Collectors.class, "toList"));
        }

        if (memberModel.isMap()) {
            return Collections.singletonList(StaticImport.staticMethodImport(Collectors.class, "toMap"));
        }

        return Collections.emptyList();
    }

    public static boolean isEnumCopyAvailable(MemberModel memberModel) {
        if (!(memberModel.isMap() || memberModel.isList())) {
            return false;
        }

        if (memberModel.isMap()) {
            MapModel mapModel = memberModel.getMapModel();
            MemberModel keyModel = mapModel.getKeyModel();
            MemberModel valueModel = mapModel.getValueModel();
            if (keyModel.getEnumType() != null || valueModel.getEnumType() != null) {
                return true;
            }

            if (valueModel.isList() || valueModel.isMap()) {
                return isEnumCopyAvailable(valueModel);
            }
            // Keys are always simple, don't need to check
        } else {
            MemberModel element = memberModel.getListModel().getListMemberModel();
            if (element.getEnumType() != null) {
                return true;
            }
            if (element.isList() || element.isMap()) {
                return isEnumCopyAvailable(element);
            }
        }

        return false;
    }

    private MethodSpec copyMethod() {
        return MethodSpec.methodBuilder(serviceModelCopiers.copyMethodName())
                         .addModifiers(Modifier.STATIC)
                         .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.NONE),
                                       memberParamName())
                         .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.NONE))
                         .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.NONE))
                         .build();
    }

    private MethodSpec enumToStringCopyMethod() {
        return MethodSpec.methodBuilder(serviceModelCopiers.enumToStringCopyMethodName())
                         .addModifiers(Modifier.STATIC)
                         .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING),
                                       memberParamName())
                         .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING))
                         .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING))
                         .build();
    }

    private MethodSpec stringToEnumCopyMethod() {
        return MethodSpec.methodBuilder(serviceModelCopiers.stringToEnumCopyMethodName())
                         .addModifiers(Modifier.STATIC)
                         .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM),
                                       memberParamName())
                         .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM))
                         .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM))
                         .build();
    }

    private MethodSpec copyFromBuilderMethod() {
        return MethodSpec.methodBuilder(serviceModelCopiers.copyFromBuilderMethodName())
                         .addModifiers(Modifier.STATIC)
                         .returns(typeName(memberModel, false, false, BuilderTransform.BUILDER_TO_BUILDABLE,
                                           EnumTransform.NONE))
                         .addParameter(typeName(memberModel, true, true, BuilderTransform.BUILDER_TO_BUILDABLE,
                                                EnumTransform.NONE),
                                       memberParamName())
                         .addCode(copyMethodBody(BuilderTransform.BUILDER_TO_BUILDABLE, EnumTransform.NONE))
                         .build();
    }

    private MethodSpec copyToBuilderMethod() {
        return MethodSpec.methodBuilder(serviceModelCopiers.copyToBuilderMethodName())
                         .addModifiers(Modifier.STATIC)
                         .returns(typeName(memberModel, false, false, BuilderTransform.BUILDABLE_TO_BUILDER, EnumTransform.NONE))
                         .addParameter(typeName(memberModel, true, true, BuilderTransform.BUILDABLE_TO_BUILDER,
                                                EnumTransform.NONE),
                                       memberParamName())
                         .addCode(copyMethodBody(BuilderTransform.BUILDABLE_TO_BUILDER, EnumTransform.NONE))
                         .build();
    }

    private CodeBlock copyMethodBody(BuilderTransform builderTransform, EnumTransform enumTransform) {
        CodeBlock.Builder code = CodeBlock.builder();

        if (!memberModel.getAutoConstructClassIfExists().isPresent()) {
            code.add("if ($N == null) {", memberParamName())
                .add("return null;")
                .add("}");
        }

        String outputVariable = copyMethodBody(code, builderTransform, new UniqueVariableSource(),
                                               enumTransform, memberParamName(), memberModel);

        code.add("return $N;", outputVariable);

        return code.build();
    }

    private String copyMethodBody(CodeBlock.Builder code, BuilderTransform builderTransform, UniqueVariableSource variableSource,
                                  EnumTransform enumTransform, String inputVariableName, MemberModel inputMember) {
        if (inputMember.getEnumType() != null) {
            String outputVariableName = variableSource.getNew("result");
            ClassName enumType = poetExtensions.getModelClass(inputMember.getEnumType());
            switch (enumTransform) {
                case NONE:
                    return inputVariableName;
                case ENUM_TO_STRING:
                    code.add("$T $N = $N.toString();", String.class, outputVariableName, inputVariableName);
                    return outputVariableName;
                case STRING_TO_ENUM:
                    code.add("$1T $2N = $1T.fromValue($3N);", enumType, outputVariableName, inputVariableName);
                    return outputVariableName;
                default:
                    throw new IllegalStateException();
            }
        }

        if (inputMember.isSimple()) {
            return inputVariableName;
        }

        if (inputMember.hasBuilder()) {
            switch (builderTransform) {
                case NONE:
                    return inputVariableName;
                case BUILDER_TO_BUILDABLE:
                    String buildableOutput = variableSource.getNew("member");
                    TypeName buildableOutputType = typeName(inputMember, false, false, builderTransform, enumTransform);
                    code.add("$T $N = $N == null ? null : $N.build();", buildableOutputType, buildableOutput, inputVariableName,
                             inputVariableName);
                    return buildableOutput;
                case BUILDABLE_TO_BUILDER:
                    String builderOutput = variableSource.getNew("member");
                    TypeName builderOutputType = typeName(inputMember, false, false, builderTransform, enumTransform);
                    code.add("$T $N = $N == null ? null : $N.toBuilder();", builderOutputType, builderOutput, inputVariableName,
                             inputVariableName);
                    return builderOutput;
                default:
                    throw new IllegalStateException();
            }
        }

        if (inputMember.isList()) {
            String outputVariableName = variableSource.getNew("list");
            String modifiableVariableName = variableSource.getNew("modifiableList");

            MemberModel listEntryModel = inputMember.getListModel().getListMemberModel();
            TypeName listType = typeName(inputMember, false, false, builderTransform, enumTransform);

            code.add("$T $N;", listType, outputVariableName)
                .add("if ($1N == null || $1N instanceof $2T) {", inputVariableName, SdkAutoConstructList.class)
                .add("$N = $T.getInstance();", outputVariableName, DefaultSdkAutoConstructList.class)
                .add("} else {");


            String entryInputVariable = variableSource.getNew("entry");

            // Short-circuit, if the member is simple then we can directly use the constructor
            // that takes a collection which should have a better performance characteristics.
            boolean isMemberSimple = listEntryModel.isSimple() && listEntryModel.getEnumType() == null;
            if (isMemberSimple) {
                code.add("$T $N = new $T<>($N);", listType, modifiableVariableName, ArrayList.class, inputVariableName);
            } else {
                code.add("$T $N = new $T<>($N.size());", listType, modifiableVariableName, ArrayList.class, inputVariableName);
                code.add("$N.forEach($N -> {", inputVariableName, entryInputVariable);

                String entryOutputVariable =
                    copyMethodBody(code, builderTransform, variableSource, enumTransform, entryInputVariable, listEntryModel);

                code.add("$N.add($N);", modifiableVariableName, entryOutputVariable)
                    .add("});");
            }
            code.add("$N = $T.unmodifiableList($N);", outputVariableName, Collections.class, modifiableVariableName)
                .add("}");

            return outputVariableName;
        }

        if (inputMember.isMap()) {
            String outputVariableName = variableSource.getNew("map");
            String modifiableVariableName = variableSource.getNew("modifiableMap");

            MemberModel keyModel = inputMember.getMapModel().getKeyModel();
            MemberModel valueModel = inputMember.getMapModel().getValueModel();
            TypeName keyType = typeName(keyModel, false, false, builderTransform, enumTransform);
            TypeName outputMapType = typeName(inputMember, false, false, builderTransform, enumTransform);

            code.add("$T $N;", outputMapType, outputVariableName)
                .add("if ($1N == null || $1N instanceof $2T) {", inputVariableName, SdkAutoConstructMap.class)
                .add("$N = $T.getInstance();", outputVariableName, DefaultSdkAutoConstructMap.class)
                .add("} else {")
                .add("$T $N = new $T<>($N.size());",
                     outputMapType, modifiableVariableName, LinkedHashMap.class, inputVariableName);

            String keyInputVariable = variableSource.getNew("key");
            String valueInputVariable = variableSource.getNew("value");
            code.add("$N.forEach(($N, $N) -> {", inputVariableName, keyInputVariable, valueInputVariable);

            String keyOutputVariable =
                copyMethodBody(code, builderTransform, variableSource, enumTransform, keyInputVariable, keyModel);

            String valueOutputVariable =
                copyMethodBody(code, builderTransform, variableSource, enumTransform, valueInputVariable, valueModel);

            if (keyModel.getEnumType() != null && !keyType.toString().equals("java.lang.String")) {
                // When enums are used as keys, drop any entries with unknown keys
                code.add("if ($N != $T.UNKNOWN_TO_SDK_VERSION) {", keyOutputVariable, keyType)
                    .add("$N.put($N, $N);", modifiableVariableName, keyOutputVariable, valueOutputVariable)
                    .add("}");
            } else {
                code.add("$N.put($N, $N);", modifiableVariableName, keyOutputVariable, valueOutputVariable);
            }

            code.add("});")
                .add("$N = $T.unmodifiableMap($N);", outputVariableName, Collections.class, modifiableVariableName)
                .add("}");

            return outputVariableName;
        }

        throw new UnsupportedOperationException("Unable to generate copier for member '" + inputMember + "'");
    }

    private TypeName typeName(MemberModel model, boolean isInputType, boolean useCollectionForList,
                              BuilderTransform builderTransform, EnumTransform enumTransform) {

        boolean useEnumTypes = (isInputType && enumTransform == EnumTransform.ENUM_TO_STRING) ||
                               (!isInputType && enumTransform == EnumTransform.STRING_TO_ENUM);

        boolean useBuilderTypes = (isInputType && builderTransform == BuilderTransform.BUILDER_TO_BUILDABLE) ||
                                  (!isInputType && builderTransform == BuilderTransform.BUILDABLE_TO_BUILDER);

        return typeProvider.typeName(model, new TypeNameOptions().useEnumTypes(useEnumTypes)
                                                                 .shapeTransformation(useBuilderTypes ? USE_BUILDER : NONE)
                                                                 .useSubtypeWildcardsForCollections(isInputType)
                                                                 .useSubtypeWildcardsForBuilders(isInputType)
                                                                 .useCollectionForList(useCollectionForList));
    }

    private String memberParamName() {
        if (memberModel.isSimple()) {
            return Utils.unCapitalize(memberModel.getVariable().getSimpleType()) + "Param";
        }
        return Utils.unCapitalize(memberModel.getC2jShape()) + "Param";
    }

    private static final class UniqueVariableSource {
        private final Map suffixes = new HashMap<>();

        private String getNew(String prefix) {
            return prefix + suffix(prefix);
        }

        private String suffix(String prefix) {
            Integer suffixNumber = suffixes.compute(prefix, (k, v) -> v == null ? 0 : v + 1);
            return suffixNumber == 0 ? "" : suffixNumber.toString();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy