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

io.ultreia.java4all.bean.spi.GenerateJavaBeanDefinitionProcessor Maven / Gradle / Ivy

There is a newer version: 0.3.3
Show newest version
package io.ultreia.java4all.bean.spi;

/*-
 * #%L
 * Java Beans extends by Ultreia.io
 * %%
 * Copyright (C) 2018 Ultreia.io
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

import com.google.auto.service.AutoService;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import io.ultreia.java4all.bean.AbstractJavaBeanComparatorBuilder;
import io.ultreia.java4all.bean.AbstractJavaBeanDefinition;
import io.ultreia.java4all.bean.AbstractJavaBeanPredicateBuilder;
import io.ultreia.java4all.bean.JavaBeanDefinition;

import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.NoType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.AbstractElementVisitor8;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.beans.Introspector;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;

/**
 * Created by tchemit on 07/01/2018.
 *
 * @author Tony Chemit - [email protected]
 */
@SupportedAnnotationTypes("io.ultreia.java4all.bean.spi.GenerateJavaBeanDefinition")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
@SupportedOptions({"debug"})
public class GenerateJavaBeanDefinitionProcessor extends AbstractProcessor {

    private static final String DEFINITION_JAVA_FILE = "package %s;\n" +
            "\n" +
            "%s" +
            "\n@AutoService(JavaBeanDefinition.class)" +
            "\n@Generated(value = \"%s\", date = \"%s\")" +
            "\npublic final class %sJavaBeanDefinition%s extends AbstractJavaBeanDefinition {\n" +
            "\n" +
            "    public %sJavaBeanDefinition() {\n" +
            "        super(ImmutableSet.>builder()\n" +
            "%s" +
            "                        .build(),\n" +
            "                ImmutableMap.>builder()\n" +
            "%s" +
            "                        .build(),\n" +
            "                ImmutableMap.>builder()\n" +
            "%s" +
            "                        .build());\n" +
            "    }\n" +
            "%s%s%s" +
            "\n" +
            "}\n";

    private static final String PREDICATE_JAVA_FILE = "\n    public static final class %s%s extends AbstractJavaBeanPredicateBuilder<%s%s, %s%s> {\n" +
            "\n        protected %s() {\n" +
            "            super(%sJavaBeanDefinition.class);\n" +
            "        }\n" +
            "\n        protected %s(%s javaBeanDefinition) {\n" +
            "            super(javaBeanDefinition);\n" +
            "        }\n" +
            "%s" +
            "\n    }\n";

    private static final String COMPARATOR_JAVA_FILE = "\n    public static final class %s%s extends AbstractJavaBeanComparatorBuilder<%s%s, %s%s> {\n" +
            "\n        protected %s() {\n" +
            "            super(%sJavaBeanDefinition.class);\n" +
            "        }\n" +
            "\n        protected %s(%s javaBeanDefinition) {\n" +
            "            super(javaBeanDefinition);\n" +
            "        }\n" +
            "%s" +
            "\n    }\n";

    @SuppressWarnings("SpellCheckingInspection")
    private static final List excludedProperties = Arrays.asList("ters", "class", "propertyChangeListeners", "vetoableChangeListeners");

    private Set done = new TreeSet<>();

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {

            Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
            for (Element annotatedElement : annotatedElements) {
                TypeElement classElement = (TypeElement) annotatedElement;

                String fullyQualifiedName = classElement.getQualifiedName().toString();

                String packageName = null;
                int lastDot = fullyQualifiedName.lastIndexOf('.');
                if (lastDot > 0) {
                    packageName = fullyQualifiedName.substring(0, lastDot);
                }

                String className = classElement.getSimpleName().toString();

                String generatedClassName = packageName + "." + className + "JavaBeanDefinition";

                if (!done.add(generatedClassName)) {

                    // Already done
                    logWarning(String.format("Skip already processed class: %s", generatedClassName));
                    continue;
                }

                logDebug(String.format("Detect JavaBean: %s", classElement));

                List typeParameters = classElement.getTypeParameters();

                Map getters = classElement.accept(new GettersDetector(processingEnv), null);
                Map setters = classElement.accept(new SettersDetector(processingEnv), null);

                String classNameWithFormal;
                String classNameWithRaw;
                if (typeParameters.isEmpty()) {
                    classNameWithFormal = "";
                    classNameWithRaw = "";
                } else {

                    List formal = new LinkedList<>();
                    List raw = new LinkedList<>();
                    for (TypeParameterElement typeParameter : typeParameters) {
                        String simpleName = typeParameter.getSimpleName().toString();
                        String type = typeParameter.getBounds().get(0).toString();
                        formal.add(simpleName);
                        raw.add(simpleName + " extends " + type);
                    }
                    classNameWithFormal = String.format("<%s>", Joiner.on(", ").join(formal));
                    classNameWithRaw = String.format("<%s>", Joiner.on(", ").join(raw));
                }

                GenerateJavaBeanDefinition realAnnotation = classElement.getAnnotation(GenerateJavaBeanDefinition.class);
                String[] classes = realAnnotation.types();
                boolean predicate = realAnnotation.predicate();
                boolean comparator = realAnnotation.comparator();
                try {
                    generateDefinitionFile(packageName, generatedClassName, className, classNameWithFormal, classNameWithRaw, getters, setters, classes, predicate, comparator);
                } catch (IOException e) {
                    throw new RuntimeException("Can't generate JavaBean definition file for: " + classElement, e);
                }

            }
        }
        return true;
    }

    private void generateDefinitionFile(String packageName, String generatedClassName, String className, String classNameWithFormal, String classNameWithRaw, Map getters, Map setters, String[] classes, boolean predicate, boolean comparator) throws IOException {
        StringBuilder typesBuilder = new StringBuilder();
        if (classes.length == 0) {
            typesBuilder.append(String.format("                        .add(%s.class)\n", className));
        } else {
            for (String aClass : classes) {
                typesBuilder.append(String.format("                        .add(%s.class)\n", aClass));
            }
        }
        String typesField = typesBuilder.toString();
        StringBuilder gettersBuilder = new StringBuilder();
        for (Map.Entry entry : getters.entrySet()) {
            String propertyName = entry.getKey();
            String methodName = entry.getValue().getSimpleName().toString();
            gettersBuilder.append(String.format("                        .put(\"%s\", %s::%s)\n", propertyName, className, methodName));
        }
        StringBuilder settersBuilder = new StringBuilder();
        for (Map.Entry entry : setters.entrySet()) {
            String propertyName = entry.getKey();
            ExecutableElement methodElement = entry.getValue();
            String methodName = methodElement.getSimpleName().toString();
            String methodType = methodElement.getParameters().get(0).asType().toString();
            settersBuilder.append(String.format("                        .put(\"%s\", (e, v) -> e.%s((%s) v))\n", propertyName, methodName, methodType));
        }

        List importList = new LinkedList<>();
        addImport(importList, packageName, AutoService.class);
        addImport(importList, packageName, Generated.class);
        addImport(importList, packageName, JavaBeanDefinition.class);
        addImport(importList, packageName, AbstractJavaBeanDefinition.class);
        addImport(importList, packageName, Function.class);
        addImport(importList, packageName, ImmutableSet.class);
        addImport(importList, packageName, BiConsumer.class);
        addImport(importList, packageName, ImmutableMap.class);

        StringBuilder extraStaticsMethods = new StringBuilder();
        StringBuilder extraMethods = new StringBuilder();
        StringBuilder extraClasses = new StringBuilder();
        if (predicate) {
            addImport(importList, packageName, AbstractJavaBeanPredicateBuilder.class);

            extraClasses.append(generatePredicateBuilderFile(className + "PredicateBuilder", className, classNameWithFormal, classNameWithRaw, getters));

            extraMethods.append(String.format("\n    @Override\n" +
                    "    public %sPredicateBuilder predicateBuilder() {\n" +
                    "        return new %sPredicateBuilder(this);\n" +
                    "    }\n", className, className));
            extraStaticsMethods.append(String.format("\n    public static %sPredicateBuilder newPredicateBuilder() {\n" +
                    "        return new %sPredicateBuilder();\n" +
                    "    }\n", className, className));

        }
        if (comparator) {
            addImport(importList, packageName, AbstractJavaBeanComparatorBuilder.class);

            extraClasses.append(generateComparatorBuilderFile(className + "ComparatorBuilder", className, classNameWithFormal, classNameWithRaw, getters));

            extraMethods.append(String.format("\n    @Override\n" +
                    "    public %sComparatorBuilder comparatorBuilder() {\n" +
                    "        return new %sComparatorBuilder(this);\n" +
                    "    }\n", className, className));
            extraStaticsMethods.append(String.format("\n    public static %sComparatorBuilder newComparatorBuilder() {\n" +
                    "        return new %sComparatorBuilder();\n" +
                    "    }\n", className, className));

        }
        StringBuilder importsBuilder = new StringBuilder();
        importList.sort(Comparator.comparing(String::toString));
        for (String s : importList) {
            importsBuilder.append("import ").append(s).append(";\n");
        }
        String imports = importsBuilder.toString();
        String content = String.format(DEFINITION_JAVA_FILE,
                packageName,
                imports,
                getClass().getName(),
                new Date(),
                className,
                classNameWithRaw,
                className,
                typesField,
                className + classNameWithFormal, gettersBuilder.toString(),
                className + classNameWithFormal, settersBuilder.toString(),
                extraStaticsMethods.toString(),
                extraMethods.toString(),
                extraClasses.toString());
        generate(generatedClassName, content);
    }

    private String generatePredicateBuilderFile(String predicateSimpleClassName, String className, String classNameWithFormal, String classNameWithRaw, Map getters) {

        List methods = new LinkedList<>();
        TypeMirror t2 = processingEnv.getTypeUtils().erasure(processingEnv.getElementUtils().getTypeElement(Comparable.class.getName()).asType());

        for (Map.Entry entry : getters.entrySet()) {
            ExecutableElement value = entry.getValue();
            TypeMirror returnType = value.getReturnType();
            boolean comparable = false;
            for (TypeMirror typeMirror : processingEnv.getTypeUtils().directSupertypes(returnType)) {
                TypeMirror erasure = processingEnv.getTypeUtils().erasure(typeMirror);
                if (t2.equals(erasure)) {
                    comparable = true;
                    break;
                }
            }
            String propertyName = entry.getKey();
            String returnTypeName;
            boolean primitiveType = returnType instanceof PrimitiveType;
            if (primitiveType) {
                returnTypeName = processingEnv.getTypeUtils().boxedClass((PrimitiveType) returnType).toString();
                comparable = true;
            } else {
                returnTypeName = returnType.toString();
            }
            boolean booleanType = returnType.toString().toLowerCase().endsWith("boolean");
            String methodName = value.getSimpleName().toString();
            if (methodName.startsWith("is")) {
                methodName = "where" + methodName.substring(2);
            } else {
                methodName = "where" + methodName.substring(3);
            }
            if (booleanType) {
                methods.add(String.format("\n" +
                                "        public %s<%s, %s> %s() {\n" +
                                "            return %s(\"%s\");\n" +
                                "        }"
                        , primitiveType ? "PrimitiveBooleanQuery" : "BooleanQuery"
                        , className + classNameWithFormal
                        , predicateSimpleClassName + classNameWithFormal
                        , methodName
                        , primitiveType ? "wherePrimitiveBoolean" : "whereBoolean"
                        , propertyName
                ));
            } else if (comparable) {
                if (returnTypeName.contains(".String")) {
                    methods.add(String.format("\n" +
                                    "        public StringQuery<%s, %s> %s() {\n" +
                                    "            return whereString(\"%s\");\n" +
                                    "        }"
                            , className + classNameWithFormal
                            , predicateSimpleClassName + classNameWithFormal
                            , methodName
                            , propertyName));
                } else {
                    methods.add(String.format("\n" +
                                    "        public %s<%s, %s, %s> %s() {\n" +
                                    "            return %s(\"%s\");\n" +
                                    "        }"
                            , primitiveType ? "PrimitiveComparableQuery" : "ComparableQuery"
                            , className + classNameWithFormal
                            , returnTypeName
                            , predicateSimpleClassName + classNameWithFormal
                            , methodName
                            , primitiveType ? "wherePrimitiveComparable" : "whereComparable"
                            , propertyName));
                }
            } else {
                methods.add(String.format("\n" +
                                "        public ObjectQuery<%s, %s, %s> %s() {\n" +
                                "            return where(\"%s\");\n" +
                                "        }"
                        , className + classNameWithFormal
                        , returnTypeName
                        , predicateSimpleClassName + classNameWithFormal
                        , methodName
                        , propertyName
                ));
            }
        }
        return String.format(PREDICATE_JAVA_FILE,

                predicateSimpleClassName,
                classNameWithRaw,
                className,
                classNameWithFormal,
                predicateSimpleClassName,
                classNameWithFormal,

                predicateSimpleClassName,
                className,
                predicateSimpleClassName,
                className + "JavaBeanDefinition",
                Joiner.on("\n").join(methods));
    }


    private String generateComparatorBuilderFile(String comparatorSimpleClassName, String className, String classNameWithFormal, String classNameWithRaw, Map getters) {

        List methods = new LinkedList<>();
        TypeMirror t2 = processingEnv.getTypeUtils().erasure(processingEnv.getElementUtils().getTypeElement(Comparable.class.getName()).asType());

        for (Map.Entry entry : getters.entrySet()) {
            ExecutableElement value = entry.getValue();
            TypeMirror returnType = value.getReturnType();
            boolean comparable = false;
            for (TypeMirror typeMirror : processingEnv.getTypeUtils().directSupertypes(returnType)) {
                TypeMirror erasure = processingEnv.getTypeUtils().erasure(typeMirror);
                if (t2.equals(erasure)) {
                    comparable = true;
                    break;
                }
            }
            String propertyName = entry.getKey();
            String returnTypeName;
            boolean primitiveType = returnType instanceof PrimitiveType;
            if (primitiveType) {
                returnTypeName = processingEnv.getTypeUtils().boxedClass((PrimitiveType) returnType).toString();
                comparable = true;
            } else {
                returnTypeName = returnType.toString();
            }
            if (comparable) {

                String methodName = value.getSimpleName().toString();
                if (methodName.startsWith("is")) {
                    methodName = "where" + methodName.substring(2);
                } else {
                    methodName = "where" + methodName.substring(3);
                }

                methods.add(String.format("\n" +
                                "        public QuerySupport<%s, %s, %s> %s() {\n" +
                                "            return on(\"%s\");\n" +
                                "        }"
                        , className + classNameWithFormal
                        , returnTypeName
                        , comparatorSimpleClassName + classNameWithFormal
                        , methodName
                        , propertyName));
            }

        }
        return String.format(COMPARATOR_JAVA_FILE,

                comparatorSimpleClassName,
                classNameWithRaw,
                className,
                classNameWithFormal,
                comparatorSimpleClassName,
                classNameWithFormal,

                comparatorSimpleClassName,
                className,
                comparatorSimpleClassName,
                className + "JavaBeanDefinition",
                Joiner.on("\n").join(methods));
    }

    private void addImport(List imports, String defaultPackage, Class type) {
        if (defaultPackage == null || !defaultPackage.equals(type.getPackage().getName())) {
            imports.add(type.getName());
        }
    }

    private void generate(String generatedClassName, String content) throws IOException {
        JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(generatedClassName);
        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
            out.print(content);
        }
    }

    private void logDebug(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
        }
    }

    private void logWarning(String msg) {
//        if (processingEnv.getOptions().containsKey("debug")) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, msg);
//        }
    }

    private static abstract class Detector extends AbstractElementVisitor8, Void> {
        final Map result = new TreeMap<>();
        private ProcessingEnvironment processingEnv;

        private Detector(ProcessingEnvironment processingEnv) {
            this.processingEnv = processingEnv;
        }

        private void logDebug(String msg) {
            if (processingEnv.getOptions().containsKey("debug")) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
            }
        }

        @Override
        public final Map visitPackage(PackageElement e, Void aVoid) {
            return null;
        }

        @Override
        public final Map visitType(TypeElement e, Void aVoid) {
            Elements elementUtils = processingEnv.getElementUtils();
            if (e.getKind() == ElementKind.CLASS) {
                TypeMirror superclass = e.getSuperclass();
                if (superclass != null) {
                    TypeElement typeElement = elementUtils.getTypeElement(superclass.toString());
                    if (typeElement != null) {
                        visitType(typeElement, aVoid);
                    }
                }
            } else if (e.getKind() == ElementKind.INTERFACE) {
                List interfaces = e.getInterfaces();
                for (TypeMirror anInterface : interfaces) {
                    TypeElement typeElement = elementUtils.getTypeElement(anInterface.toString());
                    if (typeElement != null) {
                        visitType(typeElement, aVoid);
                    }
                }
            }

            for (Element element : e.getEnclosedElements()) {
                if (element.getKind() == ElementKind.METHOD) {
                    visitExecutable((ExecutableElement) element, aVoid);
                }
            }
            return result;
        }

        @Override
        public final Map visitVariable(VariableElement e, Void aVoid) {
            return null;
        }

        protected abstract String acceptExecutable(ExecutableElement e);

        @Override
        public final Map visitExecutable(ExecutableElement e, Void aVoid) {
            String simpleName = e.getSimpleName().toString();
            String propertyName = acceptExecutable(e);
            if (propertyName != null) {
                logDebug(String.format("%s - Found a matching method: %s (%s)", getClass().getSimpleName(), simpleName, propertyName));
                result.put(propertyName, e);
            }
            return null;
        }

        @Override
        public final Map visitTypeParameter(TypeParameterElement e, Void aVoid) {
            return null;
        }
    }

    private final static class GettersDetector extends Detector {

        private GettersDetector(ProcessingEnvironment processingEnv) {
            super(processingEnv);
        }

        @Override
        protected String acceptExecutable(ExecutableElement e) {
            String simpleName = e.getSimpleName().toString();
            boolean objectPrefix = simpleName.startsWith("get");
            boolean booleanPrefix = simpleName.startsWith("is");
            boolean result = (objectPrefix || booleanPrefix)
                    && !e.getModifiers().contains(Modifier.STATIC)
                    && e.getModifiers().contains(Modifier.PUBLIC)
                    && e.getParameters().isEmpty()
                    && !(e.getReturnType() instanceof NoType);
            if (result) {
                String propertyName = Introspector.decapitalize(booleanPrefix ? simpleName.substring(2) : simpleName.substring(3));
                return excludedProperties.contains(propertyName) ? null : propertyName;
            }
            return null;
        }

    }

    private final static class SettersDetector extends Detector {

        private SettersDetector(ProcessingEnvironment processingEnv) {
            super(processingEnv);
        }

        @Override
        protected String acceptExecutable(ExecutableElement e) {
            String simpleName = e.getSimpleName().toString();
            boolean result = simpleName.startsWith("set")
                    && !e.getModifiers().contains(Modifier.STATIC)
                    && e.getModifiers().contains(Modifier.PUBLIC)
                    && e.getParameters().size() == 1
                    && e.getReturnType() instanceof NoType;
            if (result) {
                String propertyName = Introspector.decapitalize(simpleName.substring(3));
                return excludedProperties.contains(propertyName) ? null : propertyName;
            }
            return null;
        }

    }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy