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

pro.projo.generation.interfaces.InterfaceTemplateProcessor Maven / Gradle / Ivy

The newest version!
//                                                                          //
// Copyright 2019 - 2022 Mirko Raner                                        //
//                                                                          //
// 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 pro.projo.generation.interfaces;

import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.annotation.AnnotationFormatError;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Method;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import javax.annotation.Generated;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
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.TypeMirror;
import javax.lang.model.util.ElementScanner8;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.FileObject;
import org.xml.sax.SAXException;
import pro.projo.generation.ProjoProcessor;
import pro.projo.generation.ProjoTemplateFactoryGenerator;
import pro.projo.generation.dtd.DtdElementCollector;
import pro.projo.generation.dtd.DtdInputSource;
import pro.projo.generation.utilities.DefaultNameComparator;
import pro.projo.generation.utilities.MergeOptions;
import pro.projo.generation.utilities.MethodFilter;
import pro.projo.generation.utilities.PackageShortener;
import pro.projo.generation.utilities.Reduction;
import pro.projo.generation.utilities.Source;
import pro.projo.generation.utilities.Source.EnumSource;
import pro.projo.generation.utilities.Source.InterfaceSource;
import pro.projo.generation.utilities.TypeConverter;
import pro.projo.generation.utilities.TypeConverter.Type;
import pro.projo.interfaces.annotation.Dtd;
import pro.projo.interfaces.annotation.Enum;
import pro.projo.interfaces.annotation.Enums;
import pro.projo.interfaces.annotation.Interface;
import pro.projo.interfaces.annotation.Options;
import pro.projo.template.annotation.Configuration;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Stream.iterate;
import static javax.lang.model.SourceVersion.RELEASE_8;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.lang.model.type.TypeKind.NONE;
import static javax.tools.Diagnostic.Kind.ERROR;
import static javax.tools.Diagnostic.Kind.NOTE;
import static javax.tools.StandardLocation.CLASS_PATH;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Dtd;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Dtds;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Enum;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Enums;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Interface;
import static pro.projo.generation.interfaces.InterfaceTemplateProcessor.Interfaces;
import static pro.projo.generation.utilities.TypeConverter.primitives;
import static pro.projo.interfaces.annotation.Ternary.FALSE;
import static pro.projo.interfaces.annotation.Ternary.TRUE;

/**
* The {@link InterfaceTemplateProcessor} is an annotation processor that, at compile time, detects
* source files that have an {@link Interface @Interface}, {@link Enum @Enum} or {@link Dtd @Dtd}
* annotation and will use these sources for generating synthetic interfaces and enums.
*
* @author Mirko Raner
**/
@SupportedSourceVersion(RELEASE_8)
@SupportedAnnotationTypes({Dtd, Dtds, Enum, Enums, Interface, Interfaces})
public class InterfaceTemplateProcessor extends ProjoProcessor
{
    // Shortcuts for supported annotation types
    //
    // NOTE: annotations are apparently still processed even if they are not among the
    //       @SupportedAnnotationTypes, but Projo's annotations are listed nonetheless,
    //       if only to keep up with best practices...
    //
    final static String Dtd = "pro.projo.interfaces.annotation.Dtd";
    final static String Dtds = "pro.projo.interfaces.annotation.Dtds";
    final static String Enum = "pro.projo.interfaces.annotation.Enum";
    final static String Enums = "pro.projo.interfaces.annotation.Enums";
    final static String Interface = "pro.projo.interfaces.annotation.Interface";
    final static String Interfaces = "pro.projo.interfaces.annotation.Interfaces";
    final static Predicate notPrimitive = not(primitives::contains);

    private Filer filer;
    private Types types;
    private Elements elements;
    private Messager messager;

    @Override
    public Elements elements()
    {
        return elements;
    }

    @Override
    public synchronized void init(ProcessingEnvironment environment)
    {
        super.init(environment);
        filer = environment.getFiler();
        types = environment.getTypeUtils();
        elements = environment.getElementUtils();
        messager = environment.getMessager();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment round)
    {
        messager.printMessage(NOTE, "Processing interfaces...");
        process(round, this::getInterfaceConfiguration, Interface.class, $package.$InterfaceTemplate.class);
        messager.printMessage(NOTE, "Processing enums...");
        process(round, this::getEnumConfiguration, Enum.class, $package.$EnumTemplate.class);
        messager.printMessage(NOTE, "Processing DTDs...");
        process(round, this::getDtdConfiguration, Dtd.class, $package.$InterfaceTemplate.class);
        messager.printMessage(NOTE, "Done processing...");
        return true;
    }

    private <_Annotation_ extends Annotation> void process(RoundEnvironment round,
        BiFunction, Collection> configurationFactory,
        Class<_Annotation_> singleAnnotation,
        Class templateClass)
    {
        ProjoTemplateFactoryGenerator generator = new ProjoTemplateFactoryGenerator();
        Class multiAnnotation = singleAnnotation.getAnnotation(Repeatable.class).value();
        Set annotatedElements = new HashSet<>();
        annotatedElements.addAll(round.getElementsAnnotatedWith(singleAnnotation));
        annotatedElements.addAll(round.getElementsAnnotatedWith(multiAnnotation));
        for (Element element: annotatedElements)
        {
            PackageElement packageElement = (PackageElement)element;
            List<_Annotation_> annotations = getAnnotations(packageElement, singleAnnotation, multiAnnotation);
            Collection configurations = configurationFactory.apply(packageElement, annotations);
            messager.printMessage(NOTE, "Generating " + configurations.size() + " additional sources...");
            for (Configuration configuration: configurations)
            {
                String className = configuration.fullyQualifiedClassName();
                TypeElement typeElement = elements.getTypeElement(className);
                try
                {
                    String templateClassName = templateClass.getName();
                    FileObject sourceFile = createFile(filer, configuration, className, typeElement);
                    String resourceName = "/" + templateClassName.replace('.', '/') + ".java";
                    try (PrintWriter writer = new PrintWriter(sourceFile.openWriter(), true))
                    {
                        try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(resourceName)))
                        {
                            generator.generate(reader, resourceName, configuration.parameters(), configuration.postProcessor().apply(writer));
                        }
                    }
                    messager.printMessage(NOTE, "Generated " + className);
                }
                catch (IOException ioException)
                {
                    // Carry on for now; throwing an exception here would bring the compiler to a halt...
                    //
                    ioException.printStackTrace();
                    messager.printMessage(ERROR, ioException.getClass().getName() + ": " + ioException.getMessage());
                }
            }
        }
    }

    private FileObject createFile(Filer filer, Configuration configuration, String className, Element typeElement)
    throws IOException
    {
        if (configuration.isDefault(Options::fileExtension))
        {
            return filer.createSourceFile(className, typeElement);
        }
        Options options = configuration.options();
        String fileExtension = options.fileExtension();
        String sourceFileName = className.substring(className.lastIndexOf('.')+1) + fileExtension;
        String packageName = className.substring(0, className.lastIndexOf('.'));
        return filer.createResource(options.outputLocation(), packageName, sourceFileName, typeElement);
    }

    private <_Annotation_ extends Annotation> List<_Annotation_> getAnnotations(Element packageElement,
        Class<_Annotation_> single, Class repeated)
    {
        List<_Annotation_> annotations = new ArrayList<>();
        Annotation multiples = packageElement.getAnnotation(repeated);
        if (multiples != null)
        {
            try
            {
                Method value = multiples.getClass().getMethod("value");
                @SuppressWarnings("unchecked")
                _Annotation_[] repeatedAnnotations = (_Annotation_[])value.invoke(multiples);
                annotations = Arrays.asList(repeatedAnnotations);
            }
            catch (Exception exception)
            {
                messager.printMessage(ERROR, exception.getClass().getName() + ": " + exception.getMessage());
            }
        }
        Optional.ofNullable(packageElement.getAnnotation(single)).ifPresent(annotations::add);
        return annotations;
    }

    /**
    * Generates code generation configurations for declared {@link Interface @Interface} annotations.
    *
    * @param element the package in which interfaces will be generated
    * @param interfaces the set of {@link Interface @Interface} annotations containing the configuration data
    * @return a collection of code generation configurations, one for each generated interface
    **/
    private Collection getInterfaceConfiguration(PackageElement element, List interfaces)
    {
        String object = Object.class.getName();
        Name packageName = element.getQualifiedName();
        Predicate notSamePackage = name -> !name.substring(0, name.lastIndexOf('.')).equals(String.valueOf(packageName));
        Function getConfiguration = annotation ->
        {
            Set imports = new HashSet<>();
            if (new MergeOptions(element.getAnnotation(Options.class), annotation.options()).options().addAnnotations() != FALSE)
            {
                imports.add(Generated.class.getName());
            }
            TypeMirror originalClass = getTypeMirror(annotation::from);
            MethodFilter methodFilter = new MethodFilter(annotation);
            TypeElement type = elements.getTypeElement(originalClass.toString());
            List methods = new ArrayList<>();
            List typeParameters;
            if (type != null) // type does not represent a primitive
            {
                typeParameters = staticMethodsOnly(annotation)? emptyList():type.getTypeParameters();
                ElementScanner8> scanner = new ElementScanner8>()
                {
                    @Override
                    public Void visitExecutable(ExecutableElement method, List executables)
                    {
                        if (type.equals(method.getEnclosingElement())
                        && (methodFilter.matches(method)))
                        {
                            executables.add(method);
                        }
                        return super.visitExecutable(method, executables);
                    }
                };
                type.accept(scanner, methods);
            }
            else
            {
                typeParameters = emptyList();
            }
            PackageShortener shortener = new PackageShortener();

            // Include both interfaces and enums in the TypeConverter, so that references to enums from
            // within interfaces are handled properly:
            //
            InterfaceSource primary = new InterfaceSource(annotation, element.getAnnotation(Options.class));
            Stream enums = getAnnotations(element, Enum.class, Enums.class).stream().map(EnumSource::new);
            Stream sources = Stream.concat(interfaces.stream().map(InterfaceSource::new), enums);
            TypeConverter typeConverter = new TypeConverter(types, shortener, packageName, sources, primary);
            Function toDeclaration = convertToDeclaration(typeConverter, typeParameters, primary);
            Predicate validSuperclass = base -> base.getKind() != NONE && !base.toString().equals(object);
            Stream baseInterfaces = Stream.of(annotation.extend()).map(this::typeMirror);
            Stream baseInterfaceNames = baseInterfaces.map(typeConverter::convert).map(Type::signature);
            // TODO: do we need to pass unmapped or superclass flagged when converting base interfaces?
            String supertypes;
            if (type != null)
            {
                TypeMirror[] superclass = Stream.of(type.getSuperclass()).filter(validSuperclass).toArray(TypeMirror[]::new);
                supertypes = staticMethodsOnly(annotation)? "":
                    Stream.concat(concat(type.getInterfaces(), superclass).map(typeConverter::convert).map(Type::signature), baseInterfaceNames).collect(joining(", "));
            }
            else
            {
                supertypes = baseInterfaceNames.collect(joining(", "));
            }
            String[] declarations = methods.stream().filter(this::realMethodsOnly).map(toDeclaration).filter(Objects::nonNull).toArray(String[]::new);
            imports.addAll(typeConverter.getImports());
            List importNames = imports.stream().map(Object::toString)
                .filter(notPrimitive)
                .filter(notSamePackage)
                .filter(name -> !name.startsWith("java.lang."))
                .map(pro.projo.generation.utilities.Name::new)
                .sorted(new DefaultNameComparator())
                .map(Name::toString)
                .collect(toList());
            return new TemplateConfiguration(packageName, annotation.generate(), element, annotation.options())
            {
                @Override
                public Map parameters()
                {
                    Map parameters = getParameters(packageName, importNames, options());
                    parameters.put("javadoc", "This interface was extracted from " + originalClass + ".");
                    parameters.put("InterfaceTemplate", interfaceSignature());
                    parameters.put("methods", declarations);
                    return parameters;
                }

                @Override
                public UnaryOperator postProcessor()
                {
                    try
                    {
                        Class> postProcessorClass = getType(options()::postProcessor);
                        return postProcessorClass.getConstructor().newInstance();
                    }
                    catch (Exception exception)
                    {
                        throw new RuntimeException(exception);
                    }
                }

                private String interfaceSignature()
                {
                    String signature = annotation.generate();
                    if (!typeParameters.isEmpty())
                    {
                        String parameters = typeParameters.stream()
                            .map(Object::toString)
                            .map(typeVariableTransformer(primary))
                            .collect(joining(", "));
                        signature += "<" + parameters + ">";
                    }
                    if (!supertypes.isEmpty())
                    {
                        signature += " extends " + supertypes;
                    }
                    return signature;
                }
            };
        };
        return interfaces.stream().map(getConfiguration).collect(toList());
    }

    /**
    * Generates code generation configurations for declared {@link Dtd @Dtd} annotations.
    *
    * @param element the package in which interfaces will be generated
    * @param dtds the set of {@link Dtd @Dtd} annotations containing the configuration data
    * @return a collection of code generation configurations, one for each generated interface
    **/
    public Collection getDtdConfiguration(PackageElement element, List dtds)
    {
        Name packageName = element.getQualifiedName();
        Stream inputSources = dtds.stream().map
        (
            dtd ->
            {
                try
                {
                    String path = dtd.path();
                    int lastSlash = path.lastIndexOf('/');
                    String packageInfo, resourceName;
                    if (lastSlash == -1)
                    {
                        packageInfo = "";
                        resourceName = path;
                    }
                    else
                    {
                        packageInfo = path.substring(0, lastSlash);
                        resourceName = path.substring(lastSlash + 1);
                    }
                    FileObject file = filer.getResource(CLASS_PATH, packageInfo, resourceName);
                    InputStream stream = file.openInputStream();
                    DtdInputSource source = new DtdInputSource(stream, dtd);
                    source.setSystemId(file.toUri().toURL().toString());
                    return source;
                }
                catch (IOException exception)
                {
                    throw new AnnotationFormatError(exception);
                }
            }
        );
        Stream configurations = inputSources.flatMap
        (
            source ->
            {
                DtdElementCollector handler = new DtdElementCollector(packageName, source.getDtd(), elements, messager);
                try
                {
                    return handler.configurations(source);
                }
                catch (IOException|SAXException exception)
                {
                    throw new IOError(exception);
                }
            }
        );
        return configurations.collect(toList());
    }

    @SafeVarargs
    private final <_Type_> Stream<_Type_> concat(Collection initial, _Type_... additional)
    {
        return Stream.concat(initial.stream(), Stream.of(additional));
    }

    UnaryOperator typeVariableTransformer(InterfaceSource source)
    {
        try
        {
            Class> typeVariableTransformer;
            typeVariableTransformer = getType(source.options()::typeVariableTransformer);
            UnaryOperator operator = typeVariableTransformer.getConstructor().newInstance();
            return string ->
            {
                StringWriter stringWriter = new StringWriter();
                PrintWriter printWriter = new PrintWriter(operator.apply(stringWriter));
                printWriter.write(string);
                printWriter.flush();
                return stringWriter.toString();
            };
        }
        catch (Exception exception)
        {
            throw new RuntimeException(exception);
        }
    }

    private Collection getEnumConfiguration(PackageElement packageElement, List enums)
    {
        Name packageName = packageElement.getQualifiedName();
        Function getConfiguration = annotation ->
        {
            TypeElement typeElement = getTypeElement(annotation::from);
            List values = new ArrayList<>();
            ElementScanner8> scanner = new ElementScanner8>()
            {
                @Override
                public Void scan(Element element, List constants)
                {
                    if (element.getKind() == ElementKind.ENUM_CONSTANT
                    && (typeElement.equals(element.getEnclosingElement())))
                    {
                        constants.add(element.getSimpleName());
                    }
                    return super.scan(element, constants);
                }
            };
            typeElement.accept(scanner, values);
            return new TemplateConfiguration(packageName, annotation.generate(), packageElement, annotation.options())
            {
                @Override
                public Map parameters()
                {
                    Collection imports = options().addAnnotations() != FALSE? singleton(Generated.class.getName()):emptyList();
                    Map parameters = getParameters(packageName, imports, options());
                    parameters.put("javadoc", "This enum was extracted from " + typeElement.getQualifiedName() + ".");
                    parameters.put("EnumTemplate", annotation.generate());
                    parameters.put("values", values);
                    return parameters;
                }
            };
        };
        return enums.stream().map(getConfiguration).collect(toList());
    }

    Map getParameters(Name packageName, Collection imports, Options options)
    {
        String generatedBy = options.addAnnotations() != FALSE? "@Generated(\"" + getClass().getName() + "\")":"";
        Map parameters = new HashMap<>();
        parameters.put("package", packageName);
        parameters.put("imports", imports);
        parameters.put("generatedBy", generatedBy);
        return parameters;
    }

    Function convertToDeclaration(TypeConverter typeMap,
        List typeParameters, InterfaceSource primary)
    {
        return method ->
        {
            StringBuffer declaration = new StringBuffer();
    
            // Add type parameters, if any:
            List typeParameterList = method.getTypeParameters();
            Map renamedTypeVariables = renameShadowedTypeVariables(typeParameters, typeParameterList, typeVariableTransformer(primary));
            if (!typeParameterList.isEmpty())
            {
                String localTypeParameters =
                    typeParameterList.stream()
                        .map(TypeParameterElement::getSimpleName)
                        .map(Name::toString)
                        .map(name -> renamedTypeVariables.getOrDefault(name, name))
                        .collect(joining(", "));
                declaration.append("<").append(localTypeParameters).append("> ");
            }
    
            // Add return type:
            Type returnType = typeMap.convert(method.getReturnType(), renamedTypeVariables, false);
            declaration.append(returnType.signature()).append(' ');

            // Add parameters:
            declaration.append(method.getSimpleName()).append('(');
            Stream parameters = method.getParameters().stream();
            Stream> convertedParameters = parameters.map(parameter ->
            {
                Type parameterType = typeMap.convert(parameter, renamedTypeVariables);
                Name parameterName = parameter.getSimpleName();
                return new SimpleEntry<>(parameterType, parameterName);
            });
            List> convertedParameterList = convertedParameters.collect(toList());
            declaration.append(convertedParameterList.stream().map(entry -> entry.getKey().signature() + " " + entry.getValue()).collect(joining(", ")));
            boolean methodUsesUnmappedTypes = returnType.unmapped() || convertedParameterList.stream().map(Entry::getKey).map(Type::unmapped).reduce(false, Boolean::logicalOr);
            return methodUsesUnmappedTypes? null:declaration.append(')').toString();
        };
    }

    Map renameShadowedTypeVariables(
        List classLevelVariables,
        List methodLevelVariables,
        UnaryOperator transformer)
    {
    	Map typeVariableOccurrences = Stream.of(classLevelVariables, methodLevelVariables)
            .flatMap(List::stream)
            .map(TypeParameterElement::getSimpleName)
            .map(Name::toString)
            .collect(groupingBy(identity())).entrySet().stream()
            .collect(toMap(Entry::getKey, entry -> entry.getValue().size()));
        Stream duplicates = typeVariableOccurrences.entrySet().stream()
            .filter(entry -> entry.getValue() > 1)
            .map(Entry::getKey);
        Entry, Map> typesAndRenames =
            new SimpleEntry<>(typeVariableOccurrences, new HashMap<>());
        Reduction, Map>, String> renameDuplicates = (entry, type) ->
        {
        	Map types = new HashMap<>(entry.getKey());
            int suffixIndex = iterate(0, next -> next+1).filter(index -> !types.containsKey(type+index)).findFirst().get();
            String rename = transformer.apply(type + suffixIndex);
            types.put(rename, 1);
            Map renames = new HashMap<>(entry.getValue());
            renames.put(type, rename);
            return new SimpleEntry<>(types, renames);
        };
        Map rewrites =
            duplicates.reduce(typesAndRenames, renameDuplicates, (a, b) -> b).getValue();
        typeVariableOccurrences.entrySet().stream()
            .filter(entry -> !entry.getKey().equals(transformer.apply(entry.getKey())))
            .collect(toMap(Entry::getKey, entry -> transformer.apply(entry.getKey()), (a, b) -> b, () -> rewrites));
        return rewrites;
    }

    @SuppressWarnings("deprecation")
    boolean staticMethodsOnly(Interface annotation)
    {
        return annotation.isStatic() == TRUE || set(annotation.modifiers()).contains(STATIC);
    }

    boolean realMethodsOnly(ExecutableElement method)
    {
        return method.getSimpleName().charAt(0) != '<';
    }
    
    TypeElement typeElement(TypeMirror type)
    {
        return elements.getTypeElement(type.toString());
    }

    TypeMirror typeMirror(String className)
    {
        TypeElement element = elements.getTypeElement(className);
        return element.asType();
    }

    @SafeVarargs
    private final <_Any_> Set<_Any_> set(_Any_... elements)
    {
        return new HashSet<>(Arrays.asList(elements));
    }

    private static  Predicate not(Predicate target)
    {
        return target.negate();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy