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.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 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 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));
        boolean isDuration = false;
        if (metadata != null && !Strings.isNullOrEmpty(metadata.javaType())) {
            String jt = metadata.javaType();
            if ("java.time.Duration".equals(jt)) {
                isDuration = true;
            } else {
                fieldTypeName = jt;
            }
        }

        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;
        boolean isEnum;
        if (metadata != null && !Strings.isNullOrEmpty(metadata.enums())) {
            // use the order from the metadata
            enums = new LinkedHashSet<>();
            isEnum = true;
            String[] values = metadata.enums().split(",");
            for (String val : values) {
                enums.add(val.trim());
            }
        } else {
            enums = new TreeSet<>(); // sort the enums A..Z
            isEnum = fieldTypeElement.isEnum();
            if (isEnum) {
                for (Object val : fieldTypeElement.getEnumConstants()) {
                    // make the enum nicely human readable instead of typically upper cased
                    String str = val.toString();
                    str = SchemaHelper.camelCaseToDash(str);
                    enums.add(str);
                }
            }
        }

        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, isDuration);
        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;
        boolean isDuration = false;
        Metadata metadata = fieldElement.getAnnotation(Metadata.class);
        if (metadata != null && !Strings.isNullOrEmpty(metadata.javaType())) {
            String jt = metadata.javaType();
            if ("java.time.Duration".equals(jt)) {
                isDuration = true;
            } else {
                fieldTypeName = jt;
            }
        }

        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, isDuration);
        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));
            boolean isDuration = false;
            if (metadata != null && !Strings.isNullOrEmpty(metadata.javaType())) {
                String jt = metadata.javaType();
                if ("java.time.Duration".equals(jt)) {
                    isDuration = true;
                } else {
                    fieldTypeName = jt;
                }
            }

            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()) {
                        // make the enum nicely human readable instead of typically upper cased
                        String str = val.toString();
                        str = SchemaHelper.camelCaseToDash(str);
                        enums.add(str);
                    }
                }
            }

            // 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, isDuration);
            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, 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, 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, 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, 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, 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, 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, true);
        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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, false);
            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, false);
            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 "routeTemplate": 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, boolean isDuration) { 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, isDuration)); 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().isEmpty() && 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().isEmpty() && 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().isEmpty() && 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, boolean isDuration) { if (enumType) { return "enum"; } else if (isDuration) { return "duration"; } 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