You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2020/09/03 11:46:22 UTC

[camel] 01/10: CAMEL-15498: Add java source parser for discovering API methods for API based components.

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 91940489d4eb091f05a1266201cfdc1019711183
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Sep 3 09:35:47 2020 +0200

    CAMEL-15498:  Add java source parser for discovering API methods for API based components.
---
 .../maven/camel-api-component-maven-plugin/pom.xml |   8 +-
 .../maven/JavaSourceApiMethodGeneratorMojo.java    | 183 +++++++++++++++++++++
 .../org/apache/camel/maven/JavaSourceParser.java   | 163 ++++++++++++++++++
 .../org/apache/camel/component/test/TestProxy.java |  25 +++
 .../apache/camel/maven/JavaSourceParserTest.java   |  46 ++++++
 .../src/test/resources/AddressGateway.java         |  90 ++++++++++
 6 files changed, 514 insertions(+), 1 deletion(-)

diff --git a/tooling/maven/camel-api-component-maven-plugin/pom.xml b/tooling/maven/camel-api-component-maven-plugin/pom.xml
index 6b2367e..1407fae 100644
--- a/tooling/maven/camel-api-component-maven-plugin/pom.xml
+++ b/tooling/maven/camel-api-component-maven-plugin/pom.xml
@@ -92,7 +92,6 @@
             <groupId>org.sonatype.plexus</groupId>
             <artifactId>plexus-build-api</artifactId>
         </dependency>
-
         <dependency>
             <groupId>org.apache.maven.plugin-tools</groupId>
             <artifactId>maven-plugin-annotations</artifactId>
@@ -130,6 +129,13 @@
             <version>${velocity-version}</version>
         </dependency>
 
+        <!-- for java source parsing -->
+        <dependency>
+            <groupId>org.jboss.forge.roaster</groupId>
+            <artifactId>roaster-jdt</artifactId>
+            <version>${roaster-version}</version>
+        </dependency>
+
         <!-- logging -->
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
diff --git a/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceApiMethodGeneratorMojo.java b/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceApiMethodGeneratorMojo.java
new file mode 100644
index 0000000..4a2c9d3
--- /dev/null
+++ b/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceApiMethodGeneratorMojo.java
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.component.ApiMethodParser;
+import org.apache.maven.plugin.MojoExecutionException;
+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;
+
+/**
+ * Parses ApiMethod signatures from source.
+ */
+@Mojo(name = "fromSource", requiresDependencyResolution = ResolutionScope.TEST, requiresProject = true,
+      defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true)
+public class JavaSourceApiMethodGeneratorMojo extends AbstractApiMethodGeneratorMojo {
+
+    static {
+        // set Java AWT to headless before using Swing HTML parser
+        System.setProperty("java.awt.headless", "true");
+    }
+
+    protected static final String DEFAULT_EXCLUDE_PACKAGES = "javax?\\.lang.*";
+    private static final Pattern RAW_ARGTYPES_PATTERN = Pattern.compile("\\s*([^<\\s,]+)\\s*(<[^>]+>)?\\s*,?");
+
+    @Parameter(property = PREFIX + "excludePackages", defaultValue = DEFAULT_EXCLUDE_PACKAGES)
+    protected String excludePackages;
+
+    @Parameter(property = PREFIX + "excludeClasses")
+    protected String excludeClasses;
+
+    @Parameter(property = PREFIX + "includeMethods")
+    protected String includeMethods;
+
+    @Parameter(property = PREFIX + "excludeMethods")
+    protected String excludeMethods;
+
+    @Parameter(property = PREFIX + "includeStaticMethods")
+    protected Boolean includeStaticMethods;
+
+    @Override
+    public List<SignatureModel> getSignatureList() throws MojoExecutionException {
+        // signatures as map from signature with no arg names to arg names from JavadocParser
+        Map<String, SignatureModel> result = new LinkedHashMap<>();
+
+        final Pattern packagePatterns = Pattern.compile(excludePackages);
+        final Pattern classPatterns = (excludeClasses != null) ? Pattern.compile(excludeClasses) : null;
+        final Pattern includeMethodPatterns = (includeMethods != null) ? Pattern.compile(includeMethods) : null;
+        final Pattern excludeMethodPatterns = (excludeMethods != null) ? Pattern.compile(excludeMethods) : null;
+
+        // for proxy class and super classes not matching excluded packages or classes
+        for (Class<?> aClass = getProxyType();
+             aClass != null && !packagePatterns.matcher(aClass.getPackage().getName()).matches()
+                     && (classPatterns == null || !classPatterns.matcher(aClass.getSimpleName()).matches());
+             aClass = aClass.getSuperclass()) {
+
+            log.debug("Processing " + aClass.getName());
+            final String sourcePath = aClass.getName().replace('.', '/') + ".java";
+
+            // read source java text for class
+
+            try (InputStream inputStream = getProjectClassLoader().getResourceAsStream(sourcePath)) {
+                if (inputStream == null) {
+                    log.debug("Java source not found on classpath for " + aClass.getName());
+                    break;
+                }
+
+                JavaSourceParser parser = new JavaSourceParser();
+                parser.parse(inputStream);
+
+                // look for parse errors
+                final String parseError = parser.getErrorMessage();
+                if (parseError != null) {
+                    throw new MojoExecutionException(parseError);
+                }
+
+                // get public method signature
+                final Map<String, String> methodMap = parser.getMethodText();
+                for (String method : parser.getMethods()) {
+                    if (!result.containsKey(method)
+                            && (includeMethodPatterns == null || includeMethodPatterns.matcher(method).find())
+                            && (excludeMethodPatterns == null || !excludeMethodPatterns.matcher(method).find())) {
+
+                        final int leftBracket = method.indexOf('(');
+                        final String name = method.substring(0, leftBracket);
+                        final String args = method.substring(leftBracket + 1, method.length() - 1);
+                        String[] types;
+                        if (args.isEmpty()) {
+                            types = new String[0];
+                        } else {
+                            // get raw types from args
+                            final List<String> rawTypes = new ArrayList<>();
+                            final Matcher argTypesMatcher = RAW_ARGTYPES_PATTERN.matcher(args);
+                            while (argTypesMatcher.find()) {
+                                rawTypes.add(argTypesMatcher.group(1));
+                            }
+                            types = rawTypes.toArray(new String[rawTypes.size()]);
+                        }
+                        final String resultType = getResultType(aClass, name, types);
+                        if (resultType != null) {
+                            SignatureModel model = new SignatureModel();
+                            String signature = resultType + " " + name + methodMap.get(method);
+                            model.setSignature(signature);
+                            Map<String, String> params = parser.getParameters().get(name);
+                            model.setParameters(params);
+                            result.put(method, model);
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                throw new MojoExecutionException(e.getMessage(), e);
+            }
+        }
+
+        if (result.isEmpty()) {
+            throw new MojoExecutionException(
+                    "No public non-static methods found, "
+                                             + "make sure source JAR is available as project scoped=provided and optional=true dependency");
+        }
+        return new ArrayList<>(result.values());
+    }
+
+    private String getResultType(Class<?> aClass, String name, String[] types) throws MojoExecutionException {
+        Class<?>[] argTypes = new Class<?>[types.length];
+        final ClassLoader classLoader = getProjectClassLoader();
+        for (int i = 0; i < types.length; i++) {
+            try {
+                try {
+                    argTypes[i] = ApiMethodParser.forName(types[i], classLoader);
+                } catch (ClassNotFoundException e) {
+                    throw new MojoExecutionException(e.getMessage(), e);
+                }
+            } catch (IllegalArgumentException e) {
+                throw new MojoExecutionException(e.getCause().getMessage(), e.getCause());
+            }
+        }
+
+        // return null for non-public methods, and for non-static methods if includeStaticMethods is null or false
+        String result = null;
+        try {
+            final Method method = aClass.getMethod(name, argTypes);
+            int modifiers = method.getModifiers();
+            if (!Modifier.isStatic(modifiers) || Boolean.TRUE.equals(includeStaticMethods)) {
+                result = method.getReturnType().getName();
+            }
+        } catch (NoSuchMethodException e) {
+            // could be a non-public method
+            try {
+                aClass.getDeclaredMethod(name, argTypes);
+            } catch (NoSuchMethodException e1) {
+                throw new MojoExecutionException(e1.getMessage(), e1);
+            }
+        }
+
+        return result;
+    }
+
+}
diff --git a/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceParser.java b/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceParser.java
new file mode 100644
index 0000000..730d5f5
--- /dev/null
+++ b/tooling/maven/camel-api-component-maven-plugin/src/main/java/org/apache/camel/maven/JavaSourceParser.java
@@ -0,0 +1,163 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.forge.roaster.Roaster;
+import org.jboss.forge.roaster.model.JavaDocTag;
+import org.jboss.forge.roaster.model.source.JavaClassSource;
+import org.jboss.forge.roaster.model.source.MethodSource;
+import org.jboss.forge.roaster.model.source.ParameterSource;
+
+import static org.apache.camel.tooling.util.JavadocHelper.sanitizeDescription;
+
+/**
+ * Parses source java to get Method Signatures from Method Summary.
+ */
+public class JavaSourceParser {
+
+    private List<String> methods = new ArrayList<>();
+    private Map<String, String> methodText = new HashMap<>();
+    private Map<String, Map<String, String>> parameters = new LinkedHashMap<>();
+    private String errorMessage;
+
+    public synchronized void parse(InputStream in) throws Exception {
+        JavaClassSource clazz = (JavaClassSource) Roaster.parse(in);
+
+        for (MethodSource ms : clazz.getMethods()) {
+            // should not be constructor and must be public
+            if (!ms.isPublic() || ms.isConstructor()) {
+                continue;
+            }
+            String signature = ms.toSignature();
+            // roaster signatures has return values at end
+            // public create(String, AddressRequest) : Result
+
+            int pos = signature.indexOf(':');
+            if (pos != -1) {
+                String result = signature.substring(pos + 1).trim();
+                // lets use FQN types
+                result = clazz.resolveType(result);
+
+                List<JavaDocTag> params = ms.getJavaDoc().getTags("@param");
+
+                Map<String, String> docs = new LinkedHashMap<>();
+                StringBuilder sb = new StringBuilder();
+                sb.append("public ").append(result).append(" ").append(ms.getName()).append("(");
+                List<ParameterSource> list = ms.getParameters();
+                for (int i = 0; i < list.size(); i++) {
+                    ParameterSource ps = list.get(i);
+                    String name = ps.getName();
+                    String type = ps.getType().getQualifiedNameWithGenerics();
+                    if (type.startsWith("java.lang.")) {
+                        type = type.substring(10);
+                    }
+                    sb.append(type).append(" ").append(name);
+                    if (i < list.size() - 1) {
+                        sb.append(", ");
+                    }
+
+                    // need documentation for this parameter
+                    docs.put(name, getJavadocValue(params, name));
+                }
+                sb.append(");");
+
+                signature = sb.toString();
+                parameters.put(ms.getName(), docs);
+            }
+
+            methods.add(signature);
+            methodText.put(ms.getName(), signature);
+        }
+    }
+
+    private static String getJavadocValue(List<JavaDocTag> params, String name) {
+        for (JavaDocTag tag : params) {
+            String key = tag.getValue();
+            if (key.startsWith(name + " ")) {
+                String desc = key.substring(name.length() + 1);
+                desc = sanitizeJavaDocValue(desc);
+                return desc;
+            }
+        }
+        return "";
+    }
+
+    private static String sanitizeJavaDocValue(String desc) {
+        // remove leading - and whitespaces
+        while (desc.startsWith("-")) {
+            desc = desc.substring(1);
+            desc = desc.trim();
+        }
+        desc = sanitizeDescription(desc, false);
+        if (desc != null && !desc.isEmpty()) {
+            // upper case first letter
+            char ch = desc.charAt(0);
+            if (Character.isAlphabetic(ch) && !Character.isUpperCase(ch)) {
+                desc = Character.toUpperCase(ch) + desc.substring(1);
+            }
+            // remove ending dot if there is the text is just alpha or whitespace
+            boolean removeDot = true;
+            char[] arr = desc.toCharArray();
+            for (int i = 0; i < arr.length; i++) {
+                ch = arr[i];
+                boolean accept = Character.isAlphabetic(ch) || Character.isWhitespace(ch) || ch == '\''
+                        || ch == '-' || ch == '_';
+                boolean last = i == arr.length - 1;
+                accept |= last && ch == '.';
+                if (!accept) {
+                    removeDot = false;
+                    break;
+                }
+            }
+            if (removeDot && desc.endsWith(".")) {
+                desc = desc.substring(0, desc.length() - 1);
+            }
+            desc = desc.trim();
+        }
+        return desc;
+    }
+
+    public void reset() {
+        methods.clear();
+        methodText.clear();
+        parameters.clear();
+        errorMessage = null;
+    }
+
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+
+    public List<String> getMethods() {
+        return methods;
+    }
+
+    public Map<String, String> getMethodText() {
+        return methodText;
+    }
+
+    public Map<String, Map<String, String>> getParameters() {
+        return parameters;
+    }
+}
diff --git a/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/component/test/TestProxy.java b/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/component/test/TestProxy.java
index 47bedde..4772e83 100644
--- a/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/component/test/TestProxy.java
+++ b/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/component/test/TestProxy.java
@@ -21,22 +21,47 @@ import java.util.List;
 import java.util.Map;
 
 public class TestProxy {
+
+    /**
+     * Just saying hi
+     */
     public String sayHi() {
         return "Hello!";
     }
 
+    /**
+     * Just saying hi
+     *
+     * @param hello should we say hello or hi
+     */
     public String sayHi(boolean hello) {
         return hello ? "Hello!" : "Hi!";
     }
 
+    /**
+     * Just saying hi
+     *
+     * @param name your name
+     */
     public String sayHi(final String name) {
         return "Hello " + name;
     }
 
+    /**
+     * Greeting method for me
+     *
+     * @param name your name
+     */
     public final String greetMe(final String name) {
         return "Greetings " + name;
     }
 
+    /**
+     * Greeting method for us
+     *
+     * @param name1 your name
+     * @param name2 my name
+     */
     public final String greetUs(final String name1, String name2) {
         return "Greetings " + name1 + ", " + name2;
     }
diff --git a/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/maven/JavaSourceParserTest.java b/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/maven/JavaSourceParserTest.java
new file mode 100644
index 0000000..369afa0
--- /dev/null
+++ b/tooling/maven/camel-api-component-maven-plugin/src/test/java/org/apache/camel/maven/JavaSourceParserTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test JavaSourceParser.
+ */
+public class JavaSourceParserTest {
+
+    @Test
+    public void testGetMethods() throws Exception {
+        final JavaSourceParser parser = new JavaSourceParser();
+
+        parser.parse(JavaSourceParserTest.class.getResourceAsStream("/AddressGateway.java"));
+        assertEquals(4, parser.getMethods().size());
+
+        assertEquals(
+                "public com.braintreegateway.Result create(String customerId, com.braintreegateway.AddressRequest request);",
+                parser.getMethods().get(0));
+        assertEquals(2, parser.getParameters().get("create").size());
+        assertEquals("The id of the Customer", parser.getParameters().get("create").get("customerId"));
+        assertEquals("The request object", parser.getParameters().get("create").get("request"));
+
+        parser.reset();
+
+    }
+
+}
diff --git a/tooling/maven/camel-api-component-maven-plugin/src/test/resources/AddressGateway.java b/tooling/maven/camel-api-component-maven-plugin/src/test/resources/AddressGateway.java
new file mode 100644
index 0000000..14c25d7
--- /dev/null
+++ b/tooling/maven/camel-api-component-maven-plugin/src/test/resources/AddressGateway.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.braintreegateway;
+
+import com.braintreegateway.exceptions.NotFoundException;
+import com.braintreegateway.util.Http;
+import com.braintreegateway.util.NodeWrapper;
+
+/**
+ * Provides methods to create, delete, find, and update {@link Address} objects.
+ * This class does not need to be instantiated directly.
+ * Instead, use {@link BraintreeGateway#address()} to get an instance of this class:
+ *
+ * <pre>
+ * BraintreeGateway gateway = new BraintreeGateway(...);
+ * gateway.address().create(...)
+ * </pre>
+ */
+public class AddressGateway {
+
+    private Http http;
+    private Configuration configuration;
+
+    public AddressGateway(Http http, Configuration configuration) {
+        this.http = http;
+        this.configuration = configuration;
+    }
+
+    /**
+     * Creates an {@link Address} for a {@link Customer}.
+     * @param customerId the id of the {@link Customer}.
+     * @param request the request object.
+     * @return a {@link Result} object.
+     */
+    public Result<Address> create(String customerId, AddressRequest request) {
+        NodeWrapper node = http.post(configuration.getMerchantPath() + "/customers/" + customerId + "/addresses", request);
+        return new Result<Address>(node, Address.class);
+    }
+
+    /**
+     * Deletes a Customer's {@link Address}.
+     * @param customerId the id of the {@link Customer}.
+     * @param id the id of the {@link Address} to delete.
+     * @return a {@link Result} object.
+     */
+    public Result<Address> delete(String customerId, String id) {
+        http.delete(configuration.getMerchantPath() + "/customers/" + customerId + "/addresses/" + id);
+        return new Result<Address>();
+    }
+
+    /**
+     * Finds a Customer's {@link Address}.
+     * @param customerId the id of the {@link Customer}.
+     * @param id the id of the {@link Address}.
+     * @return the {@link Address} or raises a {@link com.braintreegateway.exceptions.NotFoundException}.
+     */
+    public Address find(String customerId, String id) {
+        if(customerId == null || customerId.trim().equals("") || id == null || id.trim().equals(""))
+            throw new NotFoundException();
+
+        return new Address(http.get(configuration.getMerchantPath() + "/customers/" + customerId + "/addresses/" + id));
+    }
+
+
+    /**
+     * Updates a Customer's {@link Address}.
+     * @param customerId the id of the {@link Customer}.
+     * @param id the id of the {@link Address}.
+     * @param request the request object containing the {@link AddressRequest} parameters.
+     * @return the {@link Address} or raises a {@link com.braintreegateway.exceptions.NotFoundException}.
+     */
+    public Result<Address> update(String customerId, String id, AddressRequest request) {
+        NodeWrapper node = http.put(configuration.getMerchantPath() + "/customers/" + customerId + "/addresses/" + id, request);
+        return new Result<Address>(node, Address.class);
+    }
+}