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

org.apache.camel.maven.packaging.SchemaGeneratorMojo Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.maven.packaging;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;

import org.apache.camel.maven.packaging.generics.GenericsUtil;
import org.apache.camel.spi.AsPredicate;
import org.apache.camel.spi.Metadata;
import org.apache.camel.tooling.model.EipModel;
import org.apache.camel.tooling.model.EipModel.EipOptionModel;
import org.apache.camel.tooling.model.JsonMapper;
import org.apache.camel.tooling.util.JavadocHelper;
import org.apache.camel.tooling.util.PackageHelper;
import org.apache.camel.tooling.util.Strings;
import org.apache.camel.tooling.util.srcgen.GenericType;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.source.FieldSource;
import org.jboss.forge.roaster.model.source.JavaClassSource;
import org.jboss.forge.roaster.model.source.MethodSource;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassInfo.NestingType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexReader;
import org.jboss.jandex.IndexView;

@Mojo(name = "generate-schema", threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class SchemaGeneratorMojo extends AbstractGeneratorMojo {

    public static final DotName XML_ROOT_ELEMENT = DotName.createSimple(XmlRootElement.class.getName());
    public static final DotName XML_TYPE = DotName.createSimple(XmlType.class.getName());

    // special when using expression/predicates in the model
    private static final String ONE_OF_TYPE_NAME = "org.apache.camel.model.ExpressionSubElementDefinition";
    private static final String[] ONE_OF_LANGUAGES = new String[]{"org.apache.camel.model.language.ExpressionDefinition",
        "org.apache.camel.model.language.NamespaceAwareExpression"};
    // special for inputs (these classes have sub classes, so we use this to find all classes)
    private static final String[] ONE_OF_INPUTS = new String[]{"org.apache.camel.model.ProcessorDefinition", "org.apache.camel.model.rest.VerbDefinition"};
    // special for outputs (these classes have sub classes, so we use this to
    // find all classes - and not in particular if they support outputs or not)
    private static final String[] ONE_OF_OUTPUTS = new String[]{"org.apache.camel.model.ProcessorDefinition", "org.apache.camel.model.NoOutputDefinition",
        "org.apache.camel.model.OutputDefinition", "org.apache.camel.model.OutputExpressionNode",
        "org.apache.camel.model.NoOutputExpressionNode", "org.apache.camel.model.SendDefinition",
        "org.apache.camel.model.InterceptDefinition", "org.apache.camel.model.WhenDefinition",
        "org.apache.camel.model.ToDynamicDefinition"};
    // special for verbs (these classes have sub classes, so we use this to find all classes)
    private static final String[] ONE_OF_VERBS = new String[]{"org.apache.camel.model.rest.VerbDefinition"};

    @Parameter(defaultValue = "${project.build.outputDirectory}")
    protected File classesDirectory;
    @Parameter(defaultValue = "${project.basedir}/src/generated/java")
    protected File sourcesOutputDir;
    @Parameter(defaultValue = "${project.basedir}/src/generated/resources")
    protected File resourcesOutputDir;

    private ClassLoader projectClassLoader;
    private IndexView indexView;
    private final Map sources = new HashMap<>();

    public SchemaGeneratorMojo() {
    }

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (classesDirectory == null) {
            classesDirectory = new File(project.getBuild().getOutputDirectory());
        }
        if (sourcesOutputDir == null) {
            sourcesOutputDir = new File(project.getBasedir(), "src/generated/java");
        }
        if (resourcesOutputDir == null) {
            resourcesOutputDir = new File(project.getBasedir(), "src/generated/resources");
        }
        if (!classesDirectory.isDirectory()) {
            return;
        }

        IndexView index = getIndex();

        index.getAnnotations(XML_ROOT_ELEMENT);
        //
        // @XmlRootElement
        // core elements
        //
        Set coreElements = index.getAnnotations(XML_ROOT_ELEMENT).stream()
                .filter(cpa -> cpa.target().kind() == Kind.CLASS)
                .filter(cpa -> cpa.target().asClass().nestingType() == NestingType.TOP_LEVEL)
                .filter(cpa -> cpa.target().asClass().name().toString().startsWith("org.apache.camel.model."))
                .map(cpa -> cpa.target().asClass())
                .collect(Collectors.toSet());
        if (!coreElements.isEmpty()) {
            getLog().info(String.format("Found %d core elements", coreElements.size()));
        }

        // we want them to be sorted
        for (ClassInfo element : coreElements) {
            processModelClass(element, null);
        }

        // spring elements
        Set springElements = index.getAnnotations(XML_ROOT_ELEMENT).stream()
                .filter(cpa -> cpa.target().kind() == Kind.CLASS)
                .filter(cpa -> cpa.target().asClass().nestingType() == NestingType.TOP_LEVEL)
                .filter(cpa -> {
                    String javaTypeName = cpa.target().asClass().name().toString();
                    return javaTypeName.startsWith("org.apache.camel.spring.") || javaTypeName.startsWith("org.apache.camel.core.xml.");
                })
                .map(cpa -> cpa.target().asClass())
                .collect(Collectors.toSet());
        if (!springElements.isEmpty()) {
            getLog().info(String.format("Found %d spring elements", springElements.size()));
        }

        for (ClassInfo element : springElements) {
            processModelClass(element, null);
        }
    }

    private void processModelClass(ClassInfo element, Set propertyPlaceholderDefinitions) throws MojoExecutionException {
        // skip abstract classes
        if (Modifier.isAbstract(element.flags())) {
            return;
        }
        // skip unwanted classes which are "abstract" holders
        if (element.name().toString().equals(ONE_OF_TYPE_NAME)) {
            return;
        }

        AnnotationValue annotationValue = element.classAnnotation(XML_ROOT_ELEMENT).value("name");
        String aName = annotationValue != null ? annotationValue.asString() : null;
        if (Strings.isNullOrEmpty(aName) || "##default".equals(aName)) {
            aName = element.classAnnotation(XML_TYPE).value("name").asString();
        }
        final String name = aName;

        // lets use the xsd name as the file name
        String fileName;
        if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
            fileName = element.simpleName() + PackageHelper.JSON_SUFIX;
        } else {
            fileName = name + PackageHelper.JSON_SUFIX;
        }

        String javaTypeName = element.name().toString();
        Class classElement = loadClass(javaTypeName);

        // gather eip information
        EipModel eipModel = findEipModelProperties(classElement, name);

        // get endpoint information which is divided into paths and options
        // (though there should really only be one path)
        Set eipOptions = new TreeSet<>(new EipOptionComparator(eipModel));
        findClassProperties(eipOptions, classElement, classElement, "", name);

        eipOptions.forEach(eipModel::addOption);

        // after we have found all the options then figure out if the model
        // accepts input/output
        eipModel.setInput(hasInput(classElement));
        eipModel.setOutput(hasOutput(eipModel));

        if (Strings.isNullOrEmpty(eipModel.getTitle())) {
            eipModel.setTitle(Strings.asTitle(eipModel.getName()));
        }
        if (eipModel.isOutput()) {
            // filter out outputs if we do not support it
            eipModel.getOptions().removeIf(o -> "outputs".equals(o.getName()));
        }

        // write json schema file
        String packageName = javaTypeName.substring(0, javaTypeName.lastIndexOf("."));
        String json = JsonMapper.createParameterJsonSchema(eipModel);
        updateResource(
                resourcesOutputDir.toPath(),
                packageName.replace('.', '/') + "/" + fileName,
                json);

    }

    private Class loadClass(String name) {
        try {
            return getProjectClassLoader().loadClass(name);
        } catch (ClassNotFoundException e) {
            throw (NoClassDefFoundError) new NoClassDefFoundError(name).initCause(e);
        }
    }

    private ClassLoader getProjectClassLoader() {
        if (projectClassLoader == null) {
            try {
                projectClassLoader = DynamicClassLoader.createDynamicClassLoader(project.getCompileClasspathElements());
            } catch (DependencyResolutionRequiredException e) {
                throw new RuntimeException("Unable to create project classloader", e);
            }
        }
        return projectClassLoader;
    }

    private IndexView getIndex() {
        if (indexView == null) {
            Path output = Paths.get(project.getBuild().getOutputDirectory());
            try (InputStream is = Files.newInputStream(output.resolve("META-INF/jandex.idx"))) {
                indexView = new IndexReader(is).read();
            } catch (IOException e) {
                throw new RuntimeException("IOException: " + e.getMessage(), e);
            }
        }
        return indexView;
    }

    protected EipModel findEipModelProperties(Class classElement, String name) {
        EipModel model = new EipModel();
        model.setJavaType(classElement.getName());
        model.setName(name);

        boolean deprecated = classElement.getAnnotation(Deprecated.class) != null;
        model.setDeprecated(deprecated);

        Metadata metadata = classElement.getAnnotation(Metadata.class);
        if (metadata != null) {
            if (!Strings.isNullOrEmpty(metadata.label())) {
                model.setLabel(metadata.label());
            }
            if (!Strings.isNullOrEmpty(metadata.title())) {
                model.setTitle(metadata.title());
            }
            if (!Strings.isNullOrEmpty(metadata.firstVersion())) {
                model.setFirstVersion(metadata.firstVersion());
            }
        }

        // favor to use class javadoc of component as description
        String doc = getDocComment(classElement);
        if (doc != null) {
            // need to sanitize the description first (we only want a
            // summary)
            doc = JavadocHelper.sanitizeDescription(doc, true);
            // the javadoc may actually be empty, so only change the doc
            // if we got something
            if (!Strings.isNullOrEmpty(doc)) {
                model.setDescription(doc);
            }
        }

        return model;
    }

    protected void findClassProperties(Set eipOptions,
                                       Class originalClassType, Class classElement,
                                       String prefix, String modelName) {
        while (true) {
            for (Field fieldElement : classElement.getDeclaredFields()) {

                String fieldName = fieldElement.getName();

                XmlAttribute attribute = fieldElement.getAnnotation(XmlAttribute.class);
                if (attribute != null) {
                    boolean skip = processAttribute(originalClassType, classElement, fieldElement, fieldName, attribute, eipOptions, prefix, modelName);
                    if (skip) {
                        continue;
                    }
                }

                XmlValue value = fieldElement.getAnnotation(XmlValue.class);
                if (value != null) {
                    processValue(originalClassType, classElement, fieldElement, fieldName, value, eipOptions, prefix, modelName);
                }

                XmlElements elements = fieldElement.getAnnotation(XmlElements.class);
                if (elements != null) {
                    processElements(originalClassType, classElement, elements, fieldElement, eipOptions, prefix);
                }

                XmlElement element = fieldElement.getAnnotation(XmlElement.class);
                if (element != null) {
                    processElement(originalClassType, classElement, element, fieldElement, eipOptions, prefix);
                }

                // special for eips which has outputs or requires an expressions
                XmlElementRef elementRef = fieldElement.getAnnotation(XmlElementRef.class);
                if (elementRef != null) {

                    // special for routes
                    processRoutes(originalClassType, elementRef, fieldElement, fieldName, eipOptions, prefix);

                    // special for outputs
                    processOutputs(originalClassType, elementRef, fieldElement, fieldName, eipOptions, prefix);

                    // special for when clauses (choice eip)
                    processRefWhenClauses(originalClassType, elementRef, fieldElement, fieldName, eipOptions, prefix);

                    // special for rests (rest-dsl)
                    processRests(originalClassType, elementRef, fieldElement, fieldName, eipOptions, prefix);

                    // special for verbs (rest-dsl)
                    processVerbs(originalClassType, elementRef, fieldElement, fieldName, eipOptions, prefix);

                    // special for expression
                    processRefExpression(originalClassType, classElement, elementRef, fieldElement, fieldName, eipOptions, prefix);

                }
            }

            // special when we process these nodes as they do not use JAXB
            // annotations on fields, but on methods
            if ("OptionalIdentifiedDefinition".equals(classElement.getSimpleName())) {
                processIdentified(originalClassType, classElement, eipOptions, prefix);
            } else if ("RouteDefinition".equals(classElement.getSimpleName())) {
                processRoute(originalClassType, classElement, eipOptions, prefix);
            }

            // check super classes which may also have fields
            Class superclass = classElement.getSuperclass();
            if (superclass != null) {
                classElement = superclass;
            } else {
                break;
            }
        }
    }

    private boolean processAttribute(Class originalClassType, Class classElement,
                                     Field fieldElement, String fieldName,
                                     XmlAttribute attribute, Set eipOptions,
                                     String prefix, String modelName) {
        String name = attribute.name();
        if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
            name = fieldName;
        }

        // we want to skip inheritErrorHandler which is only applicable for
        // the load-balancer
        boolean loadBalancer = "LoadBalanceDefinition".equals(originalClassType.getSimpleName().toString());
        if (!loadBalancer && "inheritErrorHandler".equals(name)) {
            return true;
        }

        Metadata metadata = fieldElement.getAnnotation(Metadata.class);

        name = prefix + name;
        Class fieldTypeElement = fieldElement.getType();
        String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));
        if (metadata != null && !Strings.isNullOrEmpty(metadata.javaType())) {
            fieldTypeName = metadata.javaType();
        }

        String defaultValue = findDefaultValue(fieldElement, fieldTypeName);
        String docComment = findJavaDoc(fieldElement, fieldName, name, classElement, true);
        boolean required = attribute.required();
        // metadata may overrule element required
        required = findRequired(fieldElement, required);

        // gather enums
        Set enums = new TreeSet<>();
        boolean isEnum;
        if (metadata != null && !Strings.isNullOrEmpty(metadata.enums())) {
            isEnum = true;
            String[] values = metadata.enums().split(",");
            for (String val : values) {
                enums.add(val.trim());
            }
        } else {
            isEnum = fieldTypeElement.isEnum();
            if (isEnum) {
                for (Object val : fieldTypeElement.getEnumConstants()) {
                    enums.add(val.toString());
                }
            }
        }

        String displayName = null;
        if (metadata != null) {
            displayName = metadata.displayName();
        }
        boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
        String deprecationNote = null;
        if (metadata != null) {
            deprecationNote = metadata.deprecationNote();
        }

        EipOptionModel ep = createOption(name, displayName, "attribute", fieldTypeName,
                required, defaultValue, docComment, deprecated, deprecationNote, isEnum, enums,
                false, null, false);
        eipOptions.add(ep);

        return false;
    }

    private void processValue(Class originalClassType, Class classElement, Field fieldElement,
                              String fieldName, XmlValue value, Set eipOptions, String prefix, String modelName) {
        // XmlValue has no name attribute
        String name = fieldName;

        if ("method".equals(modelName) || "tokenize".equals(modelName) || "xtokenize".equals(modelName)) {
            // skip expression attribute on these three languages as they are
            // solely configured using attributes
            if ("expression".equals(name)) {
                return;
            }
        }

        name = prefix + name;
        String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

        String defaultValue = findDefaultValue(fieldElement, fieldTypeName);
        String docComment = findJavaDoc(fieldElement, fieldName, name, classElement, true);
        boolean required = true;
        // metadata may overrule element required
        required = findRequired(fieldElement, required);

        String displayName = null;
        Metadata metadata = fieldElement.getAnnotation(Metadata.class);
        if (metadata != null) {
            displayName = metadata.displayName();
        }
        boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
        String deprecationNote = null;
        if (metadata != null) {
            deprecationNote = metadata.deprecationNote();
        }

        EipOptionModel ep = createOption(name, displayName, "value", fieldTypeName, required,
                defaultValue, docComment, deprecated, deprecationNote, false, null,
                false, null, false);
        eipOptions.add(ep);
    }

    private void processElement(Class originalClassType, Class classElement, XmlElement element, Field fieldElement,
                                Set eipOptions, String prefix) {
        String fieldName = fieldElement.getName();
        if (element != null) {

            Metadata metadata = fieldElement.getAnnotation(Metadata.class);

            String kind = "element";
            String name = element.name();
            if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
                name = fieldName;
            }
            name = prefix + name;
            Class fieldTypeElement = fieldElement.getType();
            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            String defaultValue = findDefaultValue(fieldElement, fieldTypeName);
            String docComment = findJavaDoc(fieldElement, fieldName, name, classElement, true);
            boolean required = element.required();
            // metadata may overrule element required
            required = findRequired(fieldElement, required);

            // is it used as predicate (check field first and then fallback to
            // its class)
            boolean asPredicate = fieldElement.getAnnotation(AsPredicate.class) != null;
            if (!asPredicate) {
                asPredicate = classElement.getAnnotation(AsPredicate.class) != null;
            }

            // gather enums
            Set enums = new TreeSet<>();
            boolean isEnum;
            if (metadata != null && !Strings.isNullOrEmpty(metadata.enums())) {
                isEnum = true;
                String[] values = metadata.enums().split(",");
                for (String val : values) {
                    enums.add(val.trim());
                }
            } else {
                isEnum = fieldTypeElement.isEnum();
                if (isEnum) {
                    for (Object val : fieldTypeElement.getEnumConstants()) {
                        enums.add(val.toString());
                    }
                }
            }

            // gather oneOf expression/predicates which uses language
            Set oneOfTypes = new TreeSet<>();
            boolean isOneOf = ONE_OF_TYPE_NAME.equals(fieldTypeName);
            if (isOneOf) {
                // okay its actually an language expression, so favor using that
                // in the eip option
                kind = "expression";
                oneOfTypes = getOneOfs(ONE_OF_LANGUAGES);
            }
            // special for otherwise as we want to indicate that the element is
            if ("otherwise".equals(name)) {
                isOneOf = true;
                oneOfTypes.add("otherwise");
            }

            String displayName = null;
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, required, defaultValue,
                    docComment, deprecated, deprecationNote, isEnum, enums, isOneOf,
                    oneOfTypes, asPredicate);
            eipOptions.add(ep);
        }
    }

    private void processElements(Class originalClassType, Class classElement, XmlElements elements, Field fieldElement,
                                 Set eipOptions, String prefix) {
        String fieldName = fieldElement.getName();
        if (elements != null) {
            String kind = "element";
            String name = fieldName;
            name = prefix + name;

            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            String defaultValue = findDefaultValue(fieldElement, fieldTypeName);
            String docComment = findJavaDoc(fieldElement, fieldName, name, classElement, true);

            boolean required = true;
            required = findRequired(fieldElement, required);

            // gather oneOf of the elements
            Set oneOfTypes = new TreeSet<>();
            for (XmlElement element : elements.value()) {
                String child = element.name();
                oneOfTypes.add(child);
            }

            String displayName = null;
            Metadata metadata = fieldElement.getAnnotation(Metadata.class);
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, required, defaultValue, docComment, deprecated, deprecationNote, false, null, true, oneOfTypes,
                    false);
            eipOptions.add(ep);
        }
    }

    private void processRoute(Class originalClassType, Class classElement,
                              Set eipOptions, String prefix) {

        // group
        String docComment = findJavaDoc(null, "group", null, classElement, true);
        EipOptionModel ep = createOption("group", "Group", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // group
        docComment = findJavaDoc(null, "streamCache", null, classElement, true);
        ep = createOption("streamCache", "Stream Cache", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // trace
        docComment = findJavaDoc(null, "trace", null, classElement, true);
        ep = createOption("trace", "Trace", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // message history
        docComment = findJavaDoc(null, "messageHistory", null, classElement, true);
        ep = createOption("messageHistory", "Message History", "attribute", "java.lang.String", false, "true", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // log mask
        docComment = findJavaDoc(null, "logMask", null, classElement, true);
        ep = createOption("logMask", "Log Mask", "attribute", "java.lang.String", false, "false", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // delayer
        docComment = findJavaDoc(null, "delayer", null, classElement, true);
        ep = createOption("delayer", "Delayer", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // autoStartup
        docComment = findJavaDoc(null, "autoStartup", null, classElement, true);
        ep = createOption("autoStartup", "Auto Startup", "attribute", "java.lang.String", false, "true", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // startupOrder
        docComment = findJavaDoc(null, "startupOrder", null, classElement, true);
        ep = createOption("startupOrder", "Startup Order", "attribute", "java.lang.Integer", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // errorHandlerRef
        docComment = findJavaDoc(null, "errorHandlerRef", null, classElement, true);
        ep = createOption("errorHandlerRef", "Error Handler", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // routePolicyRef
        docComment = findJavaDoc(null, "routePolicyRef", null, classElement, true);
        ep = createOption("routePolicyRef", "Route Policy", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // shutdownRoute
        Set enums = new LinkedHashSet<>();
        enums.add("Default");
        enums.add("Defer");
        docComment = findJavaDoc(null, "shutdownRoute", "Default", classElement, true);
        ep = createOption("shutdownRoute", "Shutdown Route", "attribute", "org.apache.camel.ShutdownRoute", false, "", docComment, false, null, true, enums, false, null, false);
        eipOptions.add(ep);

        // shutdownRunningTask
        enums = new LinkedHashSet<>();
        enums.add("CompleteCurrentTaskOnly");
        enums.add("CompleteAllTasks");
        docComment = findJavaDoc(null, "shutdownRunningTask", "CompleteCurrentTaskOnly", classElement, true);
        ep = createOption("shutdownRunningTask", "Shutdown Running Task", "attribute", "org.apache.camel.ShutdownRunningTask", false, "", docComment, false, null, true, enums,
                false, null, false);
        eipOptions.add(ep);

        // input
        Set oneOfTypes = new TreeSet<>();
        oneOfTypes.add("from");
        docComment = findJavaDoc(null, "input", null, classElement, true);
        ep = createOption("input", "Input", "element", "org.apache.camel.model.FromDefinition", true, "", docComment, false, null, false, null, true, oneOfTypes, false);
        eipOptions.add(ep);

        // outputs
        // gather oneOf which extends any of the output base classes
        oneOfTypes = new TreeSet<>();
        // find all classes that has that superClassName
        for (String superclass : ONE_OF_OUTPUTS) {
            for (ClassInfo ci : getIndex().getAllKnownSubclasses(DotName.createSimple(superclass))) {
                Class child = loadClass(ci.name().toString());
                XmlRootElement rootElement = child.getAnnotation(XmlRootElement.class);
                if (rootElement != null) {
                    String childName = rootElement.name();
                    oneOfTypes.add(childName);
                }
            }
        }

        // remove some types which are not intended as an output in eips
        oneOfTypes.remove("route");

        docComment = findJavaDoc(null, "outputs", null, classElement, true);
        ep = createOption("outputs", "Outputs", "element", "java.util.List>",
                true, "", docComment, false, null, false, null, true,
                oneOfTypes, false);
        eipOptions.add(ep);
    }

    /**
     * Special for process the OptionalIdentifiedDefinition
     */
    private void processIdentified(Class originalClassType, Class classElement,
                                   Set eipOptions, String prefix) {

        // id
        String docComment = findJavaDoc(null, "id", null, classElement, true);
        EipOptionModel ep = createOption("id", "Id", "attribute", "java.lang.String", false, "", docComment, false, null, false, null, false, null, false);
        eipOptions.add(ep);

        // description
        docComment = findJavaDoc(null, "description", null, classElement, true);
        ep = createOption("description", "Description", "element", "org.apache.camel.model.DescriptionDefinition", false, "", docComment, false, null, false, null, false, null,
                false);
        eipOptions.add(ep);
    }

    /**
     * Special for processing an @XmlElementRef routes field
     */
    private void processRoutes(Class originalClassType, XmlElementRef elementRef,
                               Field fieldElement, String fieldName,
                               Set eipOptions, String prefix) {
        if ("routes".equals(fieldName)) {

            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            Set oneOfTypes = new TreeSet<>();
            oneOfTypes.add("route");

            EipOptionModel ep = createOption("routes", "Routes", "element",
                    fieldTypeName, false, "", "Contains the Camel routes",
                    false, null, false, null, true, oneOfTypes,
                    false);
            eipOptions.add(ep);
        }
    }

    /**
     * Special for processing an @XmlElementRef rests field
     */
    private void processRests(Class originalClassType, XmlElementRef elementRef,
                              Field fieldElement, String fieldName,
                              Set eipOptions, String prefix) {
        if ("rests".equals(fieldName)) {

            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            Set oneOfTypes = new TreeSet<>();
            oneOfTypes.add("rest");

            EipOptionModel ep = createOption("rests", "Rests", "element", fieldTypeName, false, "", "Contains the rest services defined using the rest-dsl", false, null, false,
                    null, true, oneOfTypes, false);
            eipOptions.add(ep);
        }
    }

    /**
     * Special for processing an @XmlElementRef outputs field
     */
    private void processOutputs(Class originalClassType, XmlElementRef elementRef,
                                Field fieldElement, String fieldName, Set eipOptions, String prefix) {

        if ("outputs".equals(fieldName) && supportOutputs(originalClassType)) {
            String kind = "element";
            String name = elementRef.name();
            if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
                name = fieldName;
            }
            name = prefix + name;
            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            // gather oneOf which extends any of the output base classes
            Set oneOfTypes = getOneOfs(ONE_OF_OUTPUTS);

            // remove some types which are not intended as an output in eips
            oneOfTypes.remove("route");
            String displayName = null;
            Metadata metadata = fieldElement.getAnnotation(Metadata.class);
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, true, "", "", deprecated, deprecationNote, false, null, true, oneOfTypes, false);
            eipOptions.add(ep);
        }
    }

    /**
     * Special for processing an @XmlElementRef verbs field (rest-dsl)
     */
    private void processVerbs(Class originalClassType, XmlElementRef elementRef, Field fieldElement,
                              String fieldName, Set eipOptions, String prefix) {

        if ("verbs".equals(fieldName) && supportOutputs(originalClassType)) {
            String kind = "element";
            String name = elementRef.name();
            if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
                name = fieldName;
            }
            name = prefix + name;
            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            String docComment = findJavaDoc(fieldElement, fieldName, name, originalClassType, true);

            // gather oneOf which extends any of the output base classes
            Set oneOfTypes = getOneOfs(ONE_OF_VERBS);
            String displayName = null;
            Metadata metadata = fieldElement.getAnnotation(Metadata.class);
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, true, "", docComment, deprecated, deprecationNote, false, null, true, oneOfTypes, false);
            eipOptions.add(ep);
        }
    }

    /**
     * Special for processing an @XmlElementRef expression field
     */
    private void processRefExpression(Class originalClassType, Class classElement,
                                      XmlElementRef elementRef, Field fieldElement,
                                      String fieldName, Set eipOptions, String prefix) {

        if ("expression".equals(fieldName)) {
            String kind = "expression";
            String name = elementRef.name();
            if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
                name = fieldName;
            }
            name = prefix + name;
            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            // find javadoc from original class as it will override the
            // setExpression method where we can provide the javadoc for the
            // given EIP
            String docComment = findJavaDoc(fieldElement, fieldName, name, originalClassType, true);

            // is it used as predicate (check field first and then fallback to
            // its class / original class)
            boolean asPredicate = fieldElement.getAnnotation(AsPredicate.class) != null;
            if (!asPredicate) {
                asPredicate = classElement.getAnnotation(AsPredicate.class) != null;
            }
            if (!asPredicate) {
                asPredicate = originalClassType.getAnnotation(AsPredicate.class) != null;
            }

            // gather oneOf expression/predicates which uses language
            Set oneOfTypes = getOneOfs(ONE_OF_LANGUAGES);

            String displayName = null;
            Metadata metadata = fieldElement.getAnnotation(Metadata.class);
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, true, "", docComment, deprecated, deprecationNote, false, null, true, oneOfTypes, asPredicate);
            eipOptions.add(ep);
        }
    }

    private Set getOneOfs(String[] classes) {
        Set oneOfTypes = new TreeSet<>();
        for (String superclass : classes) {
            for (ClassInfo ci : getIndex().getAllKnownSubclasses(DotName.createSimple(superclass))) {
                Class child = loadClass(ci.name().toString());
                XmlRootElement rootElement = child.getAnnotation(XmlRootElement.class);
                if (rootElement != null) {
                    String childName = rootElement.name();
                    oneOfTypes.add(childName);
                }
            }
        }
        return oneOfTypes;
    }

    /**
     * Special for processing an @XmlElementRef when field
     */
    private void processRefWhenClauses(Class originalClassType, XmlElementRef elementRef,
                                       Field fieldElement, String fieldName,
                                       Set eipOptions, String prefix) {
        if ("whenClauses".equals(fieldName)) {
            String kind = "element";
            String name = elementRef.name();
            if (Strings.isNullOrEmpty(name) || "##default".equals(name)) {
                name = fieldName;
            }
            name = prefix + name;
            String fieldTypeName = getTypeName(GenericsUtil.resolveType(originalClassType, fieldElement));

            // find javadoc from original class as it will override the
            // setExpression method where we can provide the javadoc for the
            // given EIP
            String docComment = findJavaDoc(fieldElement, fieldName, name, originalClassType, true);

            // indicate that this element is one of when
            Set oneOfTypes = new HashSet<>();
            oneOfTypes.add("when");

            // when is predicate
            boolean asPredicate = true;

            String displayName = null;
            Metadata metadata = fieldElement.getAnnotation(Metadata.class);
            if (metadata != null) {
                displayName = metadata.displayName();
            }
            boolean deprecated = fieldElement.getAnnotation(Deprecated.class) != null;
            String deprecationNote = null;
            if (metadata != null) {
                deprecationNote = metadata.deprecationNote();
            }

            EipOptionModel ep = createOption(name, displayName, kind, fieldTypeName, false, "", docComment, deprecated, deprecationNote, false, null, true, oneOfTypes,
                    asPredicate);
            eipOptions.add(ep);
        }
    }

    /**
     * Whether the class supports outputs.
     * 

* There are some classes which does not support outputs, even though they * have a outputs element. */ private boolean supportOutputs(Class classElement) { return loadClass("org.apache.camel.model.OutputNode").isAssignableFrom(classElement); } private String findDefaultValue(Field fieldElement, String fieldTypeName) { String defaultValue = null; Metadata metadata = fieldElement.getAnnotation(Metadata.class); if (metadata != null) { if (!Strings.isNullOrEmpty(metadata.defaultValue())) { defaultValue = metadata.defaultValue(); } } if (defaultValue == null) { // if its a boolean type, then we use false as the default if ("boolean".equals(fieldTypeName) || "java.lang.Boolean".equals(fieldTypeName)) { defaultValue = "false"; } } return defaultValue; } private boolean findRequired(Field fieldElement, boolean defaultValue) { Metadata metadata = fieldElement.getAnnotation(Metadata.class); if (metadata != null) { return metadata.required(); } return defaultValue; } private boolean hasInput(Class classElement) { for (String name : ONE_OF_INPUTS) { if (hasSuperClass(classElement, name)) { return true; } } return false; } private boolean hasOutput(EipModel model) { switch (model.getName()) { // if we are route/rest then we accept output case "route": case "rest": return true; // special for transacted/policy which should not have output case "policy": case "transacted": return false; default: return model.getOptions().stream().anyMatch(option -> "outputs".equals(option.getName())); } } private EipOptionModel createOption(String name, String displayName, String kind, String type, boolean required, String defaultValue, String description, boolean deprecated, String deprecationNote, boolean enumType, Set enums, boolean oneOf, Set oneOfs, boolean asPredicate) { EipOptionModel option = new EipOptionModel(); option.setName(name); option.setDisplayName(Strings.isNullOrEmpty(displayName) ? Strings.asTitle(name) : displayName); option.setKind(kind); option.setRequired(required); option.setDefaultValue("java.lang.Boolean".equals(type) && !Strings.isNullOrEmpty(defaultValue) ? Boolean.parseBoolean(defaultValue) : defaultValue); option.setDescription(JavadocHelper.sanitizeDescription(description, false)); option.setDeprecated(deprecated); option.setDeprecationNote(Strings.isNullOrEmpty(deprecationNote) ? null : deprecationNote); option.setType(getType(type, enumType)); option.setJavaType(type); option.setEnums(enums != null && !enums.isEmpty() ? new ArrayList<>(enums) : null); option.setOneOfs(oneOfs != null && !oneOfs.isEmpty() ? new ArrayList<>(oneOfs) : null); option.setAsPredicate(asPredicate); return option; } private boolean hasSuperClass(Class classElement, String superClassName) { return loadClass(superClassName).isAssignableFrom(classElement); } private String findJavaDoc(Field fieldElement, String fieldName, String name, Class classElement, boolean builderPattern) { if (fieldElement != null) { Metadata md = fieldElement.getAnnotation(Metadata.class); if (md != null) { String doc = md.description(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } JavaClassSource source = javaClassSource(classElement.getName()); FieldSource field = source.getField(fieldName); if (field != null) { String doc = field.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); // special for mdcLoggingKeysPattern if ("setMdcLoggingKeysPattern".equals(setterName)) { setterName = "setMDCLoggingKeysPattern"; } for (MethodSource setter : source.getMethods()) { if (setter.getParameters().size() == 1 && setter.getName().equals(setterName)) { String doc = setter.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); for (MethodSource setter : source.getMethods()) { if (setter.getParameters().size() == 0 && setter.getName().equals(getterName)) { String doc = setter.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } if (builderPattern) { if (name != null && !name.equals(fieldName)) { for (MethodSource builder : source.getMethods()) { if (builder.getParameters().size() == 1 && builder.getName().equals(name)) { String doc = builder.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } for (MethodSource builder : source.getMethods()) { if (builder.getParameters().size() == 0 && builder.getName().equals(name)) { String doc = builder.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } } for (MethodSource builder : source.getMethods()) { if (builder.getParameters().size() == 1 && builder.getName().equals(fieldName)) { String doc = builder.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } for (MethodSource builder : source.getMethods()) { if (builder.getParameters().size() == 0 && builder.getName().equals(fieldName)) { String doc = builder.getJavaDoc().getFullText(); if (!Strings.isNullOrEmpty(doc)) { return doc; } } } } return ""; } private String getDocComment(Class classElement) { JavaClassSource source = javaClassSource(classElement.getName()); return source.getJavaDoc().getFullText(); } private JavaClassSource javaClassSource(String className) { return sources.computeIfAbsent(className, this::doParseJavaClassSource); } private JavaClassSource doParseJavaClassSource(String className) { try { Path srcDir = project.getBasedir().toPath().resolve("src/main/java"); Path file = srcDir.resolve(className.replace('.', '/') + ".java"); return (JavaClassSource) Roaster.parse(file.toFile()); } catch (IOException e) { throw new RuntimeException("Unable to parse java class " + className, e); } } private static String getTypeName(Type fieldType) { String fieldTypeName = new GenericType(fieldType).toString(); fieldTypeName = fieldTypeName.replace('$', '.'); return fieldTypeName; } /** * Gets the JSon schema type. * * @param type the java type * @return the json schema type, is never null, but returns object * as the generic type */ public static String getType(String type, boolean enumType) { if (enumType) { return "enum"; } else if (type == null) { // return generic type for unknown type return "object"; } else if (type.equals(URI.class.getName()) || type.equals(URL.class.getName())) { return "string"; } else if (type.equals(File.class.getName())) { return "string"; } else if (type.equals(Date.class.getName())) { return "string"; } else if (type.startsWith("java.lang.Class")) { return "string"; } else if (type.startsWith("java.util.List") || type.startsWith("java.util.Collection")) { return "array"; } String primitive = getPrimitiveType(type); if (primitive != null) { return primitive; } return "object"; } /** * Gets the JSon schema primitive type. * * @param name the java type * @return the json schema primitive type, or null if not a * primitive */ public static String getPrimitiveType(String name) { // special for byte[] or Object[] as its common to use if ("java.lang.byte[]".equals(name) || "byte[]".equals(name)) { return "string"; } else if ("java.lang.Byte[]".equals(name) || "Byte[]".equals(name)) { return "array"; } else if ("java.lang.Object[]".equals(name) || "Object[]".equals(name)) { return "array"; } else if ("java.lang.String[]".equals(name) || "String[]".equals(name)) { return "array"; } else if ("java.lang.Character".equals(name) || "Character".equals(name) || "char".equals(name)) { return "string"; } else if ("java.lang.String".equals(name) || "String".equals(name)) { return "string"; } else if ("java.lang.Boolean".equals(name) || "Boolean".equals(name) || "boolean".equals(name)) { return "boolean"; } else if ("java.lang.Integer".equals(name) || "Integer".equals(name) || "int".equals(name)) { return "integer"; } else if ("java.lang.Long".equals(name) || "Long".equals(name) || "long".equals(name)) { return "integer"; } else if ("java.lang.Short".equals(name) || "Short".equals(name) || "short".equals(name)) { return "integer"; } else if ("java.lang.Byte".equals(name) || "Byte".equals(name) || "byte".equals(name)) { return "integer"; } else if ("java.lang.Float".equals(name) || "Float".equals(name) || "float".equals(name)) { return "number"; } else if ("java.lang.Double".equals(name) || "Double".equals(name) || "double".equals(name)) { return "number"; } return null; } private static final class EipOptionComparator implements Comparator { private final EipModel model; private EipOptionComparator(EipModel model) { this.model = model; } @Override public int compare(EipOptionModel o1, EipOptionModel o2) { int weight = weight(o1); int weight2 = weight(o2); if (weight == weight2) { // keep the current order return 1; } else { // sort according to weight return weight2 - weight; } } private int weight(EipOptionModel o) { String name = o.getName(); // these should be first if ("expression".equals(name)) { return 10; } // these should be last if ("description".equals(name)) { return -10; } else if ("id".equals(name)) { return -9; } else if ("pattern".equals(name) && "to".equals(model.getName())) { // and pattern only for the to model return -8; } return 0; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy