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

se.arkalix.dto.DtoGenerator Maven / Gradle / Ivy

Go to download

Contains an annotation processor for automatically generating data transmission objects specified using the annotations of the kalix-dto library.

The newest version!
package se.arkalix.dto;

import com.squareup.javapoet.*;
import se.arkalix.codec.*;
import se.arkalix.dto.types.*;

import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class DtoGenerator {
    private final Map backends;

    public DtoGenerator(final DtoGeneratorBackend... backends) {
        this.backends = Arrays.stream(backends)
            .collect(Collectors.toUnmodifiableMap(DtoGeneratorBackend::codec, backend -> backend));
    }

    public void writeTo(final DtoTarget target, final String packageName, final Filer filer) throws IOException {
        final var interface_ = target.interface_();
        final var interfaceElement = interface_.element();
        final var interfaceTypeName = interfaceElement.asType();

        final var implementationClassName = target.typeName();
        final var implementation = TypeSpec.classBuilder(implementationClassName)
            .addJavadoc("{@link $T} Data Transfer Object (DTO).", interfaceTypeName)
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addSuperinterface(interfaceTypeName);

        final var builderClassName = ClassName.bestGuess("Builder");
        final var builder = TypeSpec.classBuilder(builderClassName)
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);

        final var constructor = MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PRIVATE)
            .addParameter(ParameterSpec.builder(builderClassName, "builder")
                .addModifiers(Modifier.FINAL)
                .build());

        target.properties().forEach(property -> {
            final var descriptor = property.descriptor();
            final var name = property.name();
            final var type = property.type();
            final var pInterfaceTypeName = type.originalTypeName();
            final var pGeneratedTypeName = type.generatedTypeName();

            // DTO property field.
            implementation.addField(FieldSpec.builder(
                new DtoDescriptorRouter() {
                    @Override
                    public TypeName onAny(final DtoDescriptor descriptor) {
                        return pGeneratedTypeName;
                    }

                    @Override
                    public TypeName onOptional(final DtoDescriptor descriptor) {
                        return ((DtoTypeOptional) type).valueType().generatedTypeName();
                    }
                }.route(descriptor), name)
                .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                .build());

            // DTO getter method(s).
            implementation.addMethod(new DtoDescriptorRouter() {
                private final MethodSpec.Builder getter = MethodSpec.methodBuilder(name)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(pInterfaceTypeName);

                @Override
                public MethodSpec onAny(final DtoDescriptor descriptor) {
                    return getter.addStatement("return $N", name)
                        .build();
                }

                @Override
                public MethodSpec onArray(final DtoDescriptor descriptor) {
                    return getter.addStatement("return $N.clone()", name)
                        .build();
                }

                @Override
                public MethodSpec onCollection(final DtoDescriptor descriptor) {
                    final var collection = ((DtoTypeCollection) type);
                    if (collection.containsInterfaceType()) {
                        implementation.addMethod(MethodSpec.methodBuilder(name + "AsDtos")
                            .addModifiers(Modifier.PUBLIC)
                            .addJavadoc("@see #$N()", name)
                            .returns(type.generatedTypeName())
                            .addStatement("return $N", name)
                            .build());

                        return getter
                            .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                                .addMember("value", "\"unchecked\"")
                                .build())
                            .addStatement("return ($N) $N", descriptor == DtoDescriptor.LIST ? "List" : "Map", name)
                            .build();
                    }
                    else {
                        return onAny(descriptor);
                    }
                }

                @Override
                public MethodSpec onOptional(final DtoDescriptor descriptor) {
                    final var valueType = ((DtoTypeOptional) type).valueType();
                    return new DtoDescriptorRouter() {
                        @Override
                        public MethodSpec onAny(final DtoDescriptor descriptor) {
                            return getter.addStatement("return Optional.ofNullable($N)", name)
                                .build();
                        }

                        @Override
                        public MethodSpec onArray(final DtoDescriptor descriptor) {
                            return onAny(descriptor);
                        }

                        @Override
                        public MethodSpec onCollection(final DtoDescriptor descriptor) {
                            final var collectionType = (DtoTypeCollection) valueType;
                            if (collectionType.containsInterfaceType()) {
                                implementation.addMethod(MethodSpec.methodBuilder(name + "AsDto")
                                    .addModifiers(Modifier.PUBLIC)
                                    .addJavadoc("@see #$N()", name)
                                    .returns(pGeneratedTypeName)
                                    .addStatement("return Optional.ofNullable($N)", name)
                                    .build());

                                if (descriptor == DtoDescriptor.LIST) {
                                    return getter.addStatement("return Optional.ofNullable((List) $N)", name)
                                        .build();
                                }
                                if (descriptor == DtoDescriptor.MAP) {
                                    return getter.addStatement("return Optional.ofNullable((Map) $N)", name)
                                        .build();
                                }
                            }
                            return onAny(descriptor);
                        }
                    }.route(valueType.descriptor());
                }
            }.route(descriptor));

            // Builder property field.
            builder.addField(FieldSpec.builder(
                new DtoDescriptorRouter() {
                    @Override
                    public TypeName onAny(final DtoDescriptor descriptor) {
                        return descriptor.isPrimitiveUnboxed()
                            ? pGeneratedTypeName.box()
                            : pGeneratedTypeName;
                    }

                    @Override
                    public TypeName onOptional(final DtoDescriptor descriptor) {
                        return ((DtoTypeOptional) type).valueType().generatedTypeName();
                    }
                }.route(descriptor), name)
                .addModifiers(Modifier.PRIVATE)
                .build());

            // Builder setter method(s)
            builder.addMethod(new DtoDescriptorRouter() {
                final MethodSpec.Builder setter = MethodSpec.methodBuilder(name)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(builderClassName)
                    .addStatement("this.$1N = $1N", name)
                    .addStatement("return this");

                @Override
                public MethodSpec onAny(final DtoDescriptor descriptor) {
                    return setter
                        .addParameter(pGeneratedTypeName, name, Modifier.FINAL)
                        .build();
                }

                @Override
                public MethodSpec onArray(final DtoDescriptor descriptor) {
                    setter.varargs();
                    return onAny(descriptor);
                }

                @Override
                public MethodSpec onList(final DtoDescriptor descriptor) {
                    final var itemType = ((DtoTypeSequence) type).itemType();
                    if (!itemType.descriptor().isCollection()) {
                        final var itemTypeName = itemType.generatedTypeName();
                        builder.addMethod(MethodSpec.methodBuilder(name)
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(ArrayTypeName.of(itemTypeName), name, Modifier.FINAL)
                            .varargs()
                            .returns(builderClassName)
                            .addStatement("this.$1N = $2T.asList($1N)", name, Arrays.class)
                            .addStatement("return this")
                            .build());
                    }
                    return onAny(descriptor);
                }

                @Override
                public MethodSpec onOptional(final DtoDescriptor descriptor) {
                    final var valueType = ((DtoTypeOptional) type).valueType();
                    return setter
                        .addParameter(valueType.generatedTypeName(), name, Modifier.FINAL)
                        .build();
                }
            }.route(descriptor));

            // DTO constructor statement.
            switch (property.descriptor()) {
            case ARRAY:
                constructor.addStatement("this.$1N = builder.$1N == null ? new $2T{} : builder.$1N",
                    name, pInterfaceTypeName);
                break;

            case LIST:
                constructor.addStatement("this.$1N = builder.$1N == null || builder.$1N.size() == 0 " +
                        "? $2T.emptyList() : $2T.unmodifiableList(builder.$1N)",
                    name, Collections.class);
                break;

            case MAP:
                constructor.addStatement("this.$1N = builder.$1N == null || builder.$1N.size() == 0 " +
                        "? $2T.emptyMap() : $2T.unmodifiableMap(builder.$1N)",
                    name, Collections.class);
                break;

            case OPTIONAL:
                constructor.addStatement("this.$1N = builder.$1N", name);
                break;

            default:
                constructor.addStatement("this.$1N = $2T.requireNonNull(builder.$1N, \"$1N\")",
                    name, Objects.class);
                break;
            }
        });

        if (interfaceElement.getAnnotation(DtoEqualsHashCode.class) != null) {
            final var equals = MethodSpec.methodBuilder("equals")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.OBJECT, "other", Modifier.FINAL)
                .returns(TypeName.BOOLEAN)
                .addCode("if (this == other) { return true; };\n")
                .addCode("if (other == null || getClass() != other.getClass()) { return false; };\n")
                .addCode("final $1T that = ($1T) other;\n", implementationClassName)
                .addCode("return ");

            var index = 0;
            for (final var property : target.properties()) {
                final var descriptor = property.descriptor();
                final var name = property.name();

                if (index++ != 0) {
                    equals.addCode(" &&\n    ");
                }
                if (descriptor.isPrimitiveUnboxed()) {
                    equals.addCode("$1N == that.$1N", name);
                }
                else if (descriptor == DtoDescriptor.ARRAY) {
                    equals.addCode("$1T.equals($2N, that.$2N)", Arrays.class, name);
                }
                else if (descriptor == DtoDescriptor.OPTIONAL) {
                    equals.addCode("$1T.equals($2N, that.$2N)", Objects.class, name);
                }
                else {
                    equals.addCode("$1N.equals(that.$1N)", name);
                }
            }

            implementation.addMethod(equals
                .addCode(";")
                .build());

            final var hashCode = MethodSpec.methodBuilder("hashCode")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(TypeName.INT)
                .addCode("return $T.hash(", Objects.class);

            index = 0;
            for (final var property : target.properties()) {
                if (index++ != 0) {
                    hashCode.addCode(", ");
                }
                hashCode.addCode(property.name());
            }

            implementation.addMethod(hashCode
                .addCode(");")
                .build());
        }

        if (interfaceElement.getAnnotation(DtoToString.class) != null) {
            final var toString = MethodSpec.methodBuilder("toString")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(ClassName.get(String.class))
                .addCode("return \"$T{\" +\n", interfaceTypeName);

            var index = 0;
            for (final var property : target.properties()) {
                final var name = property.name();

                if (index++ != 0) {
                    toString.addCode("    \", $N=", name);
                }
                else {
                    toString.addCode("    \"$N=", name);
                }

                switch (property.descriptor()) {
                case ARRAY:
                    toString.addCode("\" + $T.toString($N) +\n", Arrays.class, name);
                    break;

                case STRING:
                    toString.addCode("'\" + $N + '\\'' +\n", name);
                    break;

                case OPTIONAL:
                    final var valueType = ((DtoTypeOptional) property.type()).valueType();
                    if (valueType.descriptor() == DtoDescriptor.ARRAY) {
                        toString.addCode("\" + ($2N == null ? \"null\" : $1T.toString($2N)) +\n", Arrays.class, name);
                        break;

                    }
                default:
                    toString.addCode("\" + $N +\n", name);
                    break;
                }
            }

            implementation.addMethod(toString
                .addCode("    '}';\n")
                .build());
        }

        final var dtoReadableAs = interfaceElement.getAnnotation(DtoReadableAs.class);
        if (dtoReadableAs != null) {
            final var decode = MethodSpec.methodBuilder("decoder")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(ParameterizedTypeName.get(ClassName.get(Decoder.class), implementationClassName))
                .addParameter(ClassName.get(CodecType.class), "codecType", Modifier.FINAL);

            for (final var codec : dtoReadableAs.value()) {
                final var backend = getBackendByCodecOrThrow(codec);
                backend.generateDecodeMethodFor(target, implementation);
                decode
                    .beginControlFlow("if (codecType == $T.$N)", CodecType.class, codec.name())
                    .addStatement("return $T::$N", implementationClassName, backend.decodeMethodName())
                    .endControlFlow();
            }

            implementation.addMethod(decode
                .addStatement("throw new $T(codecType)", CodecUnsupported.class)
                .build());
        }

        final var dtoWritableAs = interfaceElement.getAnnotation(DtoWritableAs.class);
        if (dtoWritableAs != null) {
            final var encode = MethodSpec.methodBuilder("encodable")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(ClassName.get(Encodable.class))
                .addParameter(ClassName.get(CodecType.class), "codecType", Modifier.FINAL);

            for (final var codec : dtoWritableAs.value()) {
                final var backend = getBackendByCodecOrThrow(codec);
                backend.generateEncodeMethodFor(target, implementation);
                encode
                    .beginControlFlow("if (codecType == $T.$N)", CodecType.class, codec.name())
                    .addStatement("return this::$N", backend.encodeMethodName())
                    .endControlFlow();
            }

            implementation
                .addSuperinterface(ClassName.get(MultiEncodable.class))
                .addMethod(encode
                    .addStatement("throw new $T(codecType)", CodecUnsupported.class)
                    .build());
        }

        JavaFile.builder(packageName, implementation
            .addMethod(constructor.build())
            .addType(builder
                .addMethod(MethodSpec.methodBuilder("build")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(implementationClassName)
                    .addStatement("return new $T(this)", implementationClassName)
                    .build())
                .build())
            .build())
            .indent("    ")
            .build()
            .writeTo(filer);
    }

    private DtoGeneratorBackend getBackendByCodecOrThrow(final DtoCodec codec) {
        final var backend = backends.get(codec);
        if (backend == null) {
            throw new IllegalStateException("No code generation backend " +
                "available for codec \"" + codec + "\"; cannot generate DTO " +
                "class");
        }
        return backend;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy