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);
+ }
+}