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

io.helidon.builder.processor.CustomMethods Maven / Gradle / Ivy

/*
 * Copyright (c) 2023 Oracle and/or its affiliates.
 *
 * 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 io.helidon.builder.processor;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import io.helidon.common.Errors;
import io.helidon.common.processor.ElementInfoPredicates;
import io.helidon.common.types.Annotation;
import io.helidon.common.types.TypeInfo;
import io.helidon.common.types.TypeName;

import static io.helidon.builder.processor.Types.BUILDER_CUSTOM_METHOD_TYPE;
import static io.helidon.builder.processor.Types.CUSTOM_METHODS_TYPE;
import static io.helidon.builder.processor.Types.FACTORY_METHOD_TYPE;
import static io.helidon.builder.processor.Types.PROTOTYPE_CUSTOM_METHOD_TYPE;
import static io.helidon.builder.processor.Types.VOID_TYPE;
import static io.helidon.common.processor.classmodel.ClassModel.TYPE_TOKEN;

record CustomMethods(List factoryMethods,
                     List builderMethods,
                     List prototypeMethods) {

    CustomMethods() {
        this(List.of(), List.of(), List.of());
    }

    static CustomMethods create(ProcessingContext ctx, TypeContext.TypeInformation typeInformation) {
        Optional annotation = typeInformation.blueprintType().findAnnotation(CUSTOM_METHODS_TYPE);
        if (annotation.isEmpty()) {
            return new CustomMethods();
        }
        // value is mandatory for this annotation
        String customMethodType = annotation.get().value().orElseThrow();
        // we must get the type info, as otherwise this is an invalid declaration
        TypeInfo customMethodsInfo = ctx.toTypeInfo(TypeName.create(customMethodType))
                .orElseThrow();

        Errors.Collector errors = Errors.collector();
        List factoryMethods = findMethods(typeInformation,
                                                        customMethodsInfo,
                                                        errors,
                                                        FACTORY_METHOD_TYPE,
                                                        CustomMethods::factoryMethod);
        List builderMethods = findMethods(typeInformation,
                                                        customMethodsInfo,
                                                        errors,
                                                        BUILDER_CUSTOM_METHOD_TYPE,
                                                        CustomMethods::builderMethod);
        List prototypeMethods = findMethods(typeInformation,
                                                          customMethodsInfo,
                                                          errors,
                                                          PROTOTYPE_CUSTOM_METHOD_TYPE,
                                                          CustomMethods::prototypeMethod);

        errors.collect().checkValid();
        return new CustomMethods(factoryMethods, builderMethods, prototypeMethods);
    }

    // methods to be part of prototype interface (signature), and implement in both builder and impl
    private static GeneratedMethod prototypeMethod(Errors.Collector errors,
                                                   TypeContext.TypeInformation typeInformation,
                                                   TypeName customMethodsType,
                                                   List annotations,
                                                   Method customMethod) {
        List customMethodArgs = customMethod.arguments();
        if (customMethodArgs.isEmpty()) {
            errors.fatal(customMethodsType.fqName(),
                         "Methods annotated with @Prototype.PrototypeMethod must accept the prototype "
                                 + "as the first parameter, but method: " + customMethod.name() + " has no parameters");
        } else if (!correctType(typeInformation.prototype(), customMethodArgs.get(0).typeName())) {
            errors.fatal(customMethodsType.fqName(),
                         "Methods annotated with @Prototype.PrototypeMethod must accept the prototype "
                                 + "as the first parameter, but method: " + customMethod.name()
                                 + " expected: " + typeInformation.prototypeBuilder().fqName()
                                 + " actual: " + customMethodArgs.get(0).typeName().fqName());
        }
        List generatedArgs = customMethodArgs.subList(1, customMethodArgs.size());
        List argumentNames = new ArrayList<>();
        argumentNames.add("this");
        argumentNames.addAll(generatedArgs.stream()
                                     .map(Argument::name)
                                     .toList());

        // return CustomMethodsType.methodName(this, param1, param2)
        String generatedCall =
                VOID_TYPE.equals(customMethod.returnType) ? "" : "return "
                        + TYPE_TOKEN
                        + customMethodsType.genericTypeName().fqName()
                        + TYPE_TOKEN
                        + "."
                        + customMethod.name()
                        + "("
                        + String.join(", ", argumentNames)
                        + ")";

        return new GeneratedMethod(
                new Method(typeInformation.prototypeBuilder(),
                           customMethod.name(),
                           customMethod.returnType(),
                           generatedArgs,
                           // todo the javadoc may differ (such as when we have an additional parameter for instance methods)
                           customMethod.javadoc()),
                annotations,
                generatedCall);
    }

    // methods to be part of prototype builder only
    private static GeneratedMethod builderMethod(Errors.Collector errors,
                                                 TypeContext.TypeInformation typeInformation,
                                                 TypeName customMethodsType,
                                                 List annotations,
                                                 Method customMethod) {

        List customMethodArgs = customMethod.arguments();
        if (customMethodArgs.isEmpty()) {
            errors.fatal(customMethodsType.fqName(),
                         "Methods annotated with @Prototype.BuilderMethod must accept the prototype builder base "
                                 + "as the first parameter, but method: " + customMethod.name() + " has no parameters");
        } else if (!correctType(typeInformation.prototypeBuilderBase(), customMethodArgs.get(0).typeName().genericTypeName())) {
            errors.fatal(customMethodsType.fqName(),
                         "Methods annotated with @Prototype.BuilderMethod must accept the prototype builder "
                                 + "base as the first parameter, but method: " + customMethod.name()
                                 + " expected: " + typeInformation.prototypeBuilderBase().fqName()
                                 + " actual: " + customMethodArgs.get(0).typeName().fqName());
        }

        List generatedArgs = customMethodArgs.subList(1, customMethodArgs.size());
        List argumentNames = new ArrayList<>();
        argumentNames.add("this");
        argumentNames.addAll(generatedArgs.stream()
                                     .map(Argument::name)
                                     .toList());

        // return CustomMethodsType.methodName(this, param1, param2)
        String generatedCall = TYPE_TOKEN
                + customMethodsType.genericTypeName().fqName()
                + TYPE_TOKEN
                + "."
                + customMethod.name()
                + "("
                + String.join(", ", argumentNames)
                + ");"
                + "\nreturn self()";

        return new GeneratedMethod(
                new Method(typeInformation.prototypeBuilder(),
                           customMethod.name(),
                           typeInformation.prototypeBuilder(),
                           generatedArgs,
                           customMethod.javadoc()),
                annotations,
                generatedCall);
    }

    private static boolean correctType(TypeName knownType, TypeName processingType) {
        // processing type may be for a generated class, which does not contain package information
        if (processingType.packageName().isEmpty()) {
            if (processingType.className().equals("")) {
                // cannot be resolved as this is part of our round, good faith it is a correct parameter
                // this type name is used for types that are part of this round and that have a generic declaration
                // such as BuilderBase, also compilation will fail with a correct exception if the type is wrong
                // it will just fail on the generated class
                return true;
            }
            // the type name is known, but package could not be determined as the type is generated as part of this
            // annotation processing round - if the class name is correct, assume we have the right type
            return knownType.className().equals(processingType.className())
                    && knownType.enclosingNames().equals(processingType.enclosingNames());
        }
        return knownType.equals(processingType);
    }

    // static methods on prototype
    private static GeneratedMethod factoryMethod(Errors.Collector errors,
                                                 TypeContext.TypeInformation typeInformation,
                                                 TypeName customMethodsType,
                                                 List annotations,
                                                 Method customMethod) {

        // if void: CustomMethodsType.methodName(param1, param2)
        // if returns: return CustomMethodsType.methodName(param1, param2)
        String generatedCall =
                VOID_TYPE.equals(customMethod.returnType) ? "" : "return "
                        + TYPE_TOKEN
                        + customMethodsType.genericTypeName().fqName()
                        + TYPE_TOKEN
                        + "."
                        + customMethod.name()
                        + "("
                        + customMethod.arguments().stream().map(Argument::name).collect(Collectors.joining(", "))
                        + ")";

        // factory methods are just copied to the generated prototype
        return new GeneratedMethod(new Method(typeInformation.prototype(),
                                              customMethod.name(),
                                              customMethod.returnType(),
                                              customMethod.arguments(),
                                              customMethod.javadoc()),
                                   annotations,
                                   generatedCall);
    }

    private static List findMethods(TypeContext.TypeInformation typeInformation,
                                                  TypeInfo customMethodsType,
                                                  Errors.Collector errors,
                                                  TypeName requiredAnnotation,
                                                  MethodProcessor methodProcessor) {
        // all custom methods must be static
        // parameter and return type validation is to be done by method processor
        return customMethodsType.elementInfo()
                .stream()
                .filter(ElementInfoPredicates::isMethod)
                .filter(ElementInfoPredicates::isStatic)
                .filter(ElementInfoPredicates.hasAnnotation(requiredAnnotation))
                .map(it -> {
                    // return type
                    TypeName returnType = it.typeName();
                    // method name
                    String methodName = it.elementName();
                    // parameters
                    List arguments = it.parameterArguments()
                            .stream()
                            .map(arg -> new Argument(arg.elementName(), arg.typeName()))
                            .toList();

                    // javadoc, if present
                    List javadoc = it.description()
                            .map(String::trim)
                            .stream()
                            .filter(Predicate.not(String::isBlank))
                            .findAny()
                            .map(description -> description.split("\n"))
                            .map(List::of)
                            .orElseGet(List::of);

                    // annotations to be added to generated code
                    List annotations = it.findAnnotation(Types.PROTOTYPE_ANNOTATED_TYPE)
                            .flatMap(Annotation::stringValues)
                            .orElseGet(List::of)
                            .stream()
                            .map(String::trim) // to remove spaces after commas when used
                            .filter(Predicate.not(String::isBlank)) // we do not care about blank values
                            .toList();

                    Method customMethod = new Method(customMethodsType.typeName(), methodName, returnType, arguments, javadoc);

                    return new CustomMethod(customMethod,
                                            methodProcessor.process(errors,
                                                                    typeInformation,
                                                                    customMethodsType.typeName(),
                                                                    annotations,
                                                                    customMethod));
                })
                .toList();
    }

    interface MethodProcessor {
        GeneratedMethod process(Errors.Collector collector,
                                TypeContext.TypeInformation typeInformation,
                                TypeName customMethodsType,
                                List annotations,
                                Method customMethod);
    }

    record CustomMethod(Method declaredMethod,
                        GeneratedMethod generatedMethod) {

    }

    record Method(TypeName declaringType,
                  String name,
                  TypeName returnType,
                  List arguments,
                  List javadoc) {

    }

    record GeneratedMethod(Method method,
                           List annotations,
                           String callCode) {

    }

    record Argument(String name,
                    TypeName typeName) {

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy