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

io.syndesis.extension.maven.annotation.processing.ActionProcessor Maven / Gradle / Ivy

There is a newer version: 1.13.2
Show newest version
/*
 * Copyright (C) 2016 Red Hat, Inc.
 *
 * 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.syndesis.extension.maven.annotation.processing;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Locale;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
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.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

@SuppressWarnings("PMD")
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({
    ActionProcessor.SYNDESIS_ANNOTATION_CLASS_NAME
})
public class ActionProcessor extends AbstractProcessor {
    public static final String SYNDESIS_ANNOTATION_CLASS_NAME = "io.syndesis.extension.api.annotations.Action";
    public static final String SYNDESIS_PROPERTY_ANNOTATION_CLASS_NAME = "io.syndesis.extension.api.annotations.ConfigurationProperty";
    public static final String SYNDESIS_PROPERTY_ENUM_ANNOTATION_CLASS_NAME = "io.syndesis.extension.api.annotations.ConfigurationProperty$PropertyEnum";
    public static final String SYNDESIS_STEP_CLASS_NAME = "io.syndesis.extension.api.Step";
    public static final String CAMEL_HANDLER_ANNOTATION_CLASS_NAME = "org.apache.camel.Handler";
    public static final String CAMEL_ROUTE_BUILDER_CLASS_NAME_ = "org.apache.camel.builder.RouteBuilder";
    public static final String BEAN_ANNOTATION_CLASS_NAME = "org.springframework.context.annotation.Bean";

    private ObjectMapper mapper;
    private Class annotationClass;
    private Class propertyAnnotationClass;
    private Class propertyEnumAnnotationClass;
    private Class beanAnnotationClass;
    private Class handlerAnnotationClass;
    private Class routeBuilderClass;
    private Class stepClass;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mapper = new ObjectMapper()
            .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
            .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);;

        annotationClass = mandatoryFindClass(SYNDESIS_ANNOTATION_CLASS_NAME);
        propertyAnnotationClass = mandatoryFindClass(SYNDESIS_PROPERTY_ANNOTATION_CLASS_NAME);
        propertyEnumAnnotationClass = mandatoryFindClass(SYNDESIS_PROPERTY_ENUM_ANNOTATION_CLASS_NAME);
        stepClass = findClass(SYNDESIS_STEP_CLASS_NAME);
        beanAnnotationClass = findClass(BEAN_ANNOTATION_CLASS_NAME);
        handlerAnnotationClass = mandatoryFindClass(CAMEL_HANDLER_ANNOTATION_CLASS_NAME);
        routeBuilderClass = mandatoryFindClass(CAMEL_ROUTE_BUILDER_CLASS_NAME_);
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment env) {
        // a lot of noisy logic to prevent this method to ever fail, since it's required by the compiler implicit contract
        if(annotationClass == null){
            return false;
        }

        // If true the annotation types are claimed and subsequent processors
        // will not be asked to process them.
        boolean claimed = false;

        for (Element annotatedElement : env.getElementsAnnotatedWith(annotationClass)) {
            if (annotatedElement.getKind() == ElementKind.CLASS) {
                try {
                    ObjectNode root = mapper.createObjectNode();

                    gatherProperties(root, annotatedElement.getAnnotation(annotationClass));

                    claimed = augmentProperties(root, (TypeElement) annotatedElement);
                    addActionProperties(root, annotatedElement);

                    File file = obtainResourceFile(annotatedElement);
                    if (file != null) {
                        mapper.writerWithDefaultPrettyPrinter().writeValue(file, root);
                    }
                } catch (IOException|InvocationTargetException|IllegalAccessException|NoSuchMethodException e){
                    claimed = false;
                }
            } else if (annotatedElement.getKind() == ElementKind.METHOD) {
                try {
                    ObjectNode root = mapper.createObjectNode();

                    gatherProperties(root, annotatedElement.getAnnotation(annotationClass));

                    augmentProperties(root, (ExecutableElement) annotatedElement);
                    addActionProperties(root, annotatedElement);

                    File file = obtainResourceFile(annotatedElement);
                    if (file != null) {
                        mapper.writerWithDefaultPrettyPrinter().writeValue(file, root);
                    }
                } catch (IOException|InvocationTargetException|IllegalAccessException|NoSuchMethodException e){
                    claimed = false;
                }
            } else {
                claimed = true;
            }
        }

        return claimed;
    }

    // *****************************************
    // Annotation processing
    // *****************************************

    /**
     * Explicitly add properties that elude reflection implicit strategy
     * @param element
     * @param root
     */
    private boolean augmentProperties(ObjectNode root, TypeElement element) throws InvocationTargetException, IllegalAccessException {
        final Elements elements = processingEnv.getElementUtils();
        final TypeElement stepTypeElement = elements.getTypeElement(stepClass.getName());
        final TypeElement routeBuilderTypeElement = elements.getTypeElement(routeBuilderClass.getName());

        if (stepTypeElement != null && processingEnv.getTypeUtils().isAssignable(element.asType(), stepTypeElement.asType())) {
            root.put("kind", "STEP");
            root.put("entrypoint", element.getQualifiedName().toString());

            // Let's search for fields annotated with ConfigurationProperty
            for (Element field: element.getEnclosedElements()) {
                if (field.getKind() == ElementKind.FIELD) {
                    addActionProperties(root, field);
                }
            }

            return true;
        } else if (routeBuilderTypeElement != null && processingEnv.getTypeUtils().isAssignable(element.asType(), routeBuilderTypeElement.asType())) {
            root.put("kind", "ENDPOINT");
            root.put("resource", "class:" + element.getQualifiedName().toString());

            // Let's search for fields annotated with ConfigurationProperty
            for (Element field: element.getEnclosedElements()) {
                if (field.getKind() == ElementKind.FIELD) {
                    addActionProperties(root, field);
                }
            }
        } else {
            root.put("kind", "BEAN");
            root.put("entrypoint", element.getQualifiedName().toString());

            for (Element method: element.getEnclosedElements()) {
                if (method.getAnnotation(handlerAnnotationClass) != null) {
                    // Process method
                    augmentProperties(root, (ExecutableElement)method);
                    addActionProperties(root, method);

                    // Found a method annotated with Handler, let's search for
                    // fields annotated with ConfigurationProperty
                    for (Element field: element.getEnclosedElements()) {
                        if (field.getKind() == ElementKind.FIELD) {
                            addActionProperties(root, field);
                        }
                    }

                    // No need to go ahead as this is the method that implements
                    // the action
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Explicitly add properties that elude reflection implicit strategy
     * @param root
     * @param element
     */
    @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
    private void augmentProperties(ObjectNode root, ExecutableElement element) {
        final TypeElement typedElement = (TypeElement) element.getEnclosingElement();

        if (beanAnnotationClass != null && element.getAnnotation(beanAnnotationClass) != null) {
            root.put("kind", "ENDPOINT");
        } else {
            root.put("kind", "BEAN");
            root.put("entrypoint", typedElement.getQualifiedName().toString() + "::" + element.getSimpleName());
        }
    }

    /**
     * Add action properties to the global properties.
     * @param root
     * @param element
     */
    private void addActionProperties(ObjectNode root, Element element) throws InvocationTargetException, IllegalAccessException {

        Annotation[] annotations = element.getAnnotationsByType(propertyAnnotationClass);
        for (int i = 0; i < annotations.length; i++) {
            Annotation annotation = annotations[i];
            ObjectNode propertyNode = mapper.createObjectNode();

            gatherProperties(propertyNode, annotation);

            if (element.getKind() == ElementKind.FIELD) {
                VariableElement field = (VariableElement)element;

                TypeMirror typeMirror = field.asType();
                TypeElement typeElement = processingEnv.getElementUtils().getTypeElement(typeMirror.toString());
                String javaType = typeMirror.toString();
                String type = propertyNode.get("type").asText();

                if (!propertyNode.has("enums")) {
                    // don't auto detect enum if enums are set through annotations
                    if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) {
                        for (Element enumElement : typeElement.getEnclosedElements()) {
                            if (enumElement.getKind() == ElementKind.ENUM_CONSTANT) {
                                ObjectNode enumNode = mapper.createObjectNode();

                                writeIfNotEmpty(enumNode, "label", enumElement.toString());
                                writeIfNotEmpty(enumNode, "value", enumElement.toString());

                                propertyNode.withArray("enums").add(enumNode);
                            }
                        }

                        javaType = String.class.getName();
                        type = String.class.getName();
                    }
                }

                if (type == null || "".equals(type.trim())){
                    if (String.class.getName().equals(type)) {
                        type = "string";
                    } else if (Boolean.class.getName().equals(type)) {
                        type = "boolean";
                    } else if (Integer.class.getName().equals(type)) {
                        type = "int";
                    } else if (Float.class.getName().equals(type)) {
                        type = "float";
                    } else if (Double.class.getName().equals(type)) {
                        type = "double";
                    }
                }

                writeIfNotEmpty(propertyNode, "javaType", javaType);
                writeIfNotEmpty(propertyNode, "type", type);
            }

            root.withArray("properties").add(propertyNode);
        }
    }

    private void gatherProperties(ObjectNode root, Annotation annotation) throws InvocationTargetException, IllegalAccessException  {
        Method[] methods = annotation.annotationType().getDeclaredMethods();
        for (Method m : methods) {
            if (m.getReturnType().isAnnotation()) {
                ObjectNode node = root.putObject(m.getName());

                gatherProperties(node, (Annotation)m.invoke(annotation));
            } else {
                writeIfNotEmpty(root, m.getName(), m.invoke(annotation));
            }
        }
    }

    private void writeIfNotEmpty(ObjectNode root, String key, Object value) throws InvocationTargetException, IllegalAccessException {
        if(value != null && !"".equals(value.toString().trim())) {
            if(value instanceof String[]){
                String[] arr = (String[])value;

                if(arr.length > 0) {
                    ArrayNode arrayNode = root.putArray(key);
                    for (String val: arr) {
                        arrayNode.add(val);
                    }
                }
            } else if(Object[].class.isInstance(value)) {
                Object[] array = (Object[]) value;
                for (int i = 0; i < array.length; i++) {
                    if (propertyEnumAnnotationClass.isInstance(array[i])) {
                        Annotation enumAnnotation = (Annotation) array[i];
                        ObjectNode enumNode = mapper.createObjectNode();

                        gatherProperties(enumNode, enumAnnotation);

                        root.withArray(key).add(enumNode);
                    }
                }
            } else {
                root.put(key, value.toString());
            }
        }
    }

    // *****************************************
    // Helpers
    // *****************************************

    /**
     * Helper method to produce class output text file using the given handler
     */
    @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
    private File obtainResourceFile(Element element) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        TypeElement classElement;
        if (element instanceof TypeElement) {
            classElement = (TypeElement)element;
        } else if (element instanceof ExecutableElement) {
            classElement = (TypeElement)element.getEnclosingElement();
        } else {
            warning("Unsupported element kind: " + element.getKind());
            return null;
        }

        final String javaTypeName = canonicalClassName(classElement.getQualifiedName().toString());
        final String packageName = javaTypeName.substring(0, javaTypeName.lastIndexOf('.'));
        final Annotation annotation = element.getAnnotation(annotationClass);

        if (annotation == null) {
            error("Annotation SyndesisExtensionAction not found processing element " + element);
        }

        final String actionId = (String)annotationClass.getMethod("id").invoke(annotation);
        final String fileName = new StringBuilder()
            .append(classElement.getSimpleName().toString())
            .append('-')
            .append(Names.sanitize(actionId))
            .append(".json")
            .toString();

        File result = null;
        Filer filer = processingEnv.getFiler();
        FileObject resource;
        try {
            resource = filer.getResource(StandardLocation.SOURCE_OUTPUT, packageName, fileName);
        } catch (Exception e) {
            resource = filer.createResource(StandardLocation.SOURCE_OUTPUT, packageName, fileName);
        }
        URI uri = resource.toUri();
        if (uri != null) {
            try {
                result = new File(uri.getPath());
            } catch (Exception e) {
                warning("Cannot convert output directory resource URI to a file " + e);
            }
        }
        if (result == null) {
            warning("No class output directory could be found!");
        } else {
            result.getParentFile().mkdirs();
        }
        return result;
    }

    /**
     * Returns the canonical class name by removing any generic type information.
     */
    private static String canonicalClassName(String className) {
        // remove generics
        int pos = className.indexOf('<');
        if (pos != -1) {
            return className.substring(0, pos);
        } else {
            return className;
        }
    }

    private void info(String format, Object... args) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, String.format(format, args));
    }

    private void warning(String format, Object... args) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, String.format(format, args));
    }

    private void error(String format, Object... args) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(format, args));
    }

    private Class mandatoryFindClass(String name) {
        try {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            Class ret = (Class) Class.forName(name);
            return ret;
        } catch (ClassNotFoundException e) {
            error("Unable to find Class " +  name + " on Classpath");
        }

        return null;
    }

    private Class findClass(String name) {
        try {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            Class ret = (Class) Class.forName(name);
            return ret;
        } catch (ClassNotFoundException e) {
            warning("Unable to find Class " +  name + " on Classpath");
        }

        return null;
    }

    // From app/rest/core
    private static final class Names {
        private static final String INVALID_CHARACTER_REGEX = "[^a-zA-Z0-9-]";
        private static final String SPACE = " ";
        private static final String BLANK = "";
        private static final String DASH = "-";

        /**
         * Sanitizes the specified name by applying the following rules:
         * 1. Keep the first 100 characters.
         * 2. Replace spaces with dashes.
         * 3. Remove invalid characters.
         * @param name  The specified name.
         * @return      The sanitized string.
         */
        static String sanitize(String name) {
            return name
                .replaceAll(SPACE, DASH)
                .replaceAll(INVALID_CHARACTER_REGEX, BLANK)
                .toLowerCase(Locale.US)
                .chars()
                .filter(i -> !String.valueOf(i).matches(INVALID_CHARACTER_REGEX))
                .collect(StringBuilder::new,
                    (b, chr) -> {
                        int lastChar = b.length() > 0 ? b.charAt(b.length() - 1) : -1;

                        if (lastChar != '-' || chr != '-') {
                            b.appendCodePoint(chr);
                        }
                    }, StringBuilder::append)
                .toString();
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy