Repository: camel Updated Branches: refs/heads/master 8526fab4b -> d485f2f00
CAMEL-10197: Added support for creating Spring auto configuration inner classes to avoid flattening endpoint options into component classes Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/b26d81c8 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/b26d81c8 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/b26d81c8 Branch: refs/heads/master Commit: b26d81c891af4185505a42dd85d454ff9e5eceea Parents: 8526fab Author: Dhiraj Bokde <dhira...@yahoo.com> Authored: Wed Aug 31 09:05:35 2016 -0700 Committer: Dhiraj Bokde <dhira...@yahoo.com> Committed: Tue Sep 20 07:30:23 2016 -0700 ---------------------------------------------------------------------- .../maven/camel-package-maven-plugin/pom.xml | 4 + .../SpringBootAutoConfigurationMojo.java | 256 +++++++++++++++---- 2 files changed, 212 insertions(+), 48 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/b26d81c8/tooling/maven/camel-package-maven-plugin/pom.xml ---------------------------------------------------------------------- diff --git a/tooling/maven/camel-package-maven-plugin/pom.xml b/tooling/maven/camel-package-maven-plugin/pom.xml index 7ad0e39..de96be4 100644 --- a/tooling/maven/camel-package-maven-plugin/pom.xml +++ b/tooling/maven/camel-package-maven-plugin/pom.xml @@ -130,6 +130,10 @@ <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>spi-annotations</artifactId> + </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/camel/blob/b26d81c8/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/SpringBootAutoConfigurationMojo.java ---------------------------------------------------------------------- diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/SpringBootAutoConfigurationMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/SpringBootAutoConfigurationMojo.java index 1ca1616..3a3f9f4 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/SpringBootAutoConfigurationMojo.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/SpringBootAutoConfigurationMojo.java @@ -18,16 +18,24 @@ package org.apache.camel.maven.packaging; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.camel.maven.packaging.model.ComponentModel; @@ -37,6 +45,8 @@ import org.apache.camel.maven.packaging.model.DataFormatOptionModel; import org.apache.camel.maven.packaging.model.EndpointOptionModel; import org.apache.camel.maven.packaging.model.LanguageModel; import org.apache.camel.maven.packaging.model.LanguageOptionModel; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriPath; import org.apache.commons.io.FileUtils; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; @@ -44,7 +54,11 @@ import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.JavaEnum; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.Type; import org.jboss.forge.roaster.model.source.AnnotationSource; +import org.jboss.forge.roaster.model.source.FieldSource; import org.jboss.forge.roaster.model.source.Import; import org.jboss.forge.roaster.model.source.JavaClassSource; import org.jboss.forge.roaster.model.source.MethodSource; @@ -54,6 +68,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,6 +79,7 @@ import static org.apache.camel.maven.packaging.PackageHelper.loadText; * Generate Spring Boot auto configuration files for Camel components and data formats. * * @goal prepare-spring-boot-auto-configuration + * @requiresDependencyResolution compile+runtime */ public class SpringBootAutoConfigurationMojo extends AbstractMojo { @@ -76,6 +92,31 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { private static final boolean DELETE_FILES_ON_MAIN_ARTIFACTS = false; /** + * Suffix used for generating inner classes for nested component properties, e.g. endpoint configuration. + */ + private static final String INNER_TYPE_SUFFIX = "NestedConfiguration"; + + /** + * Classes to exclude when adding {@link NestedConfigurationProperty} annotations. + */ + private static final Pattern EXCLUDE_CLASSES_PATTERN = Pattern.compile("^((java\\.)|(javax\\.)).*"); + + private static final Map<String, String> PRIMITIVEMAP; + + static { + PRIMITIVEMAP = new HashMap<>(); + PRIMITIVEMAP.put("boolean", "java.lang.Boolean"); + PRIMITIVEMAP.put("char", "java.lang.Character"); + PRIMITIVEMAP.put("long", "java.lang.Long"); + PRIMITIVEMAP.put("int", "java.lang.Integer"); + PRIMITIVEMAP.put("integer", "java.lang.Integer"); + PRIMITIVEMAP.put("byte", "java.lang.Byte"); + PRIMITIVEMAP.put("short", "java.lang.Short"); + PRIMITIVEMAP.put("double", "java.lang.Double"); + PRIMITIVEMAP.put("float", "java.lang.Float"); + } + + /** * The maven project. * * @parameter property="project" @@ -282,6 +323,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { prefix = prefix.toLowerCase(Locale.US); javaClass.addAnnotation("org.springframework.boot.context.properties.ConfigurationProperties").setStringValue("prefix", prefix); + Set<JavaClassSource> nestedTypes = new HashSet<>(); for (ComponentOptionModel option : model.getComponentOptions()) { if (skipComponentOption(model, option)) { @@ -289,25 +331,19 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { continue; } - // remove <?> as generic type as Roaster (Eclipse JDT) cannot use that String type = option.getJavaType(); - type = type.replaceAll("\\<\\?\\>", ""); - // use wrapper types for primitive types so a null mean that the option has not been configured - if ("boolean".equals(type)) { - type = "java.lang.Boolean"; - } else if ("int".equals(type) || "integer".equals(type)) { - type = "java.lang.Integer"; - } else if ("byte".equals(type)) { - type = "java.lang.Byte"; - } else if ("short".equals(type)) { - type = "java.lang.Short"; - } else if ("double".equals(type)) { - type = "java.lang.Double"; - } else if ("float".equals(type)) { - type = "java.lang.Float"; + type = getSimpleJavaType(type); + + // generate inner class for non-primitive options + if (isNestedProperty(type, project, nestedTypes)) { + type = option.getShortJavaType() + INNER_TYPE_SUFFIX; } PropertySource<JavaClassSource> prop = javaClass.addProperty(type, option.getName()); + if (!type.endsWith(INNER_TYPE_SUFFIX) && !EXCLUDE_CLASSES_PATTERN.matcher(type).matches() && Strings.isBlank(option.getEnumValues())) { + // add nested configuration annotation for complex properties + prop.getField().addAnnotation(NestedConfigurationProperty.class); + } if ("true".equals(option.getDeprecated())) { prop.getField().addAnnotation(Deprecated.class); prop.getAccessor().addAnnotation(Deprecated.class); @@ -331,6 +367,77 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { } } + // add inner classes for nested AutoConfiguration options + ClassLoader projectClassLoader = getProjectClassLoader(); + for (JavaClassSource nestedType : nestedTypes) { + + final JavaClassSource innerClass = javaClass.addNestedType("public static class " + nestedType.getName() + INNER_TYPE_SUFFIX); + // add source class name as a static field + innerClass.addField() + .setPublic() + .setStatic(true) + .setFinal(true) + .setType(Class.class) + .setName("CAMEL_NESTED_CLASS") + .setLiteralInitializer(nestedType.getCanonicalName() + ".class"); + + // parse option type + for (PropertySource<JavaClassSource> sourceProp : nestedType.getProperties()) { + + final Type<JavaClassSource> sourcePropType = sourceProp.getType(); + final MethodSource<JavaClassSource> mutator = sourceProp.getMutator(); + // NOTE: fields with no setters are skipped + if (mutator == null) { + continue; + } + + final String optionType = getSimpleJavaType(sourcePropType.getQualifiedNameWithGenerics()); + final FieldSource<JavaClassSource> field = sourceProp.getField(); + + + final PropertySource<JavaClassSource> prop = innerClass.addProperty(optionType, sourceProp.getName()); + // add nested configuration annotation for complex properties + if (!EXCLUDE_CLASSES_PATTERN.matcher(optionType).matches() && !isEnum(projectClassLoader, optionType)) { + prop.getField().addAnnotation(NestedConfigurationProperty.class); + } + if (sourceProp.hasAnnotation(Deprecated.class)) { + prop.getField().addAnnotation(Deprecated.class); + prop.getAccessor().addAnnotation(Deprecated.class); + prop.getMutator().addAnnotation(Deprecated.class); + // DeprecatedConfigurationProperty must be on getter when deprecated + prop.getAccessor().addAnnotation(DeprecatedConfigurationProperty.class); + } + + String description = null; + if (mutator.hasJavaDoc()) { + description = mutator.getJavaDoc().getFullText(); + } else if (field != null) { + description = field.getJavaDoc().getFullText(); + } + if (!Strings.isBlank(description)) { + prop.getField().getJavaDoc().setFullText(description); + } + + String defaultValue = null; + if (sourceProp.hasAnnotation(UriParam.class)) { + defaultValue = sourceProp.getAnnotation(UriParam.class).getStringValue("defaultValue"); + } else if (sourceProp.hasAnnotation(UriPath.class)) { + defaultValue = sourceProp.getAnnotation(UriPath.class).getStringValue("defaultValue"); + } + if (!Strings.isBlank(defaultValue)) { + if ("java.lang.String".equals(optionType)) { + prop.getField().setStringInitializer(defaultValue); + } else if ("integer".equals(optionType) || "boolean".equals(optionType)) { + prop.getField().setLiteralInitializer(defaultValue); + } else if (isEnum(projectClassLoader, optionType)) { + String enumShortName = optionType.substring(optionType.lastIndexOf(".") + 1); + prop.getField().setLiteralInitializer(enumShortName + "." + defaultValue); + javaClass.addImport(model.getJavaType()); + } + } + } + } + sortImports(javaClass); String fileName = packageName.replaceAll("\\.", "\\/") + "/" + name + ".java"; @@ -338,6 +445,73 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { writeSourceIfChanged(javaClass, fileName); } + private boolean isEnum(ClassLoader projectClassLoader, String optionType) throws MojoFailureException { + // remove array brackets + try { + return projectClassLoader.loadClass(optionType.replaceAll("[\\[\\]]", "")).isEnum(); + } catch (ClassNotFoundException e) { + throw new MojoFailureException(e.getMessage(), e); + } + } + + protected ClassLoader getProjectClassLoader() throws MojoFailureException { + final List classpathElements; + try { + classpathElements = project.getTestClasspathElements(); + } catch (org.apache.maven.artifact.DependencyResolutionRequiredException e) { + throw new MojoFailureException(e.getMessage(), e); + } + final URL[] urls = new URL[classpathElements.size()]; + int i = 0; + for (Iterator it = classpathElements.iterator(); it.hasNext(); i++) { + try { + urls[i] = new File((String) it.next()).toURI().toURL(); + } catch (MalformedURLException e) { + throw new MojoFailureException(e.getMessage(), e); + } + } + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + return new URLClassLoader(urls, tccl != null ? tccl : getClass().getClassLoader()); + } + + private String getSimpleJavaType(String type) { + // remove <?> as generic type as Roaster (Eclipse JDT) cannot use that + type = type.replaceAll("\\<\\?\\>", ""); + // use wrapper types for primitive types so a null mean that the option has not been configured + String primitive = type.replaceAll("[\\[\\]]", ""); + String wrapper = PRIMITIVEMAP.get(primitive); + if (wrapper != null) { + type = type.replaceAll(primitive, wrapper); + } + return type; + } + + // it's a nested property if the source exists in this project, e.g. endpoint configuration + private boolean isNestedProperty(String type, MavenProject project, Set<JavaClassSource> nestedTypes) { + boolean nested = false; + if (!type.startsWith("java.lang.")) { + + final String fileName = type.replaceAll("[\\[\\]]", "").replaceAll("\\.", "\\/") + ".java"; + for (Object sourceRoot : project.getCompileSourceRoots()) { + + File sourceFile = new File(sourceRoot.toString(), fileName); + if (sourceFile.isFile()) { + try { + JavaType<?> classSource = Roaster.parse(sourceFile); + if (classSource instanceof JavaClassSource) { + nestedTypes.add((JavaClassSource) classSource); + nested = true; + break; + } + } catch (FileNotFoundException e) { + throw new IllegalArgumentException("Missing source file " + type); + } + } + } + } + return nested; + } + // CHECKSTYLE:OFF private static boolean skipComponentOption(ComponentModel model, ComponentOptionModel option) { if ("netty4-http".equals(model.getScheme()) || "netty-http".equals(model.getScheme())) { @@ -377,23 +551,8 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { if ("id".equals(option.getName())) { continue; } - // remove <?> as generic type as Roaster (Eclipse JDT) cannot use that String type = option.getJavaType(); - type = type.replaceAll("\\<\\?\\>", ""); - // use wrapper types for primitive types so a null mean that the option has not been configured - if ("boolean".equals(type)) { - type = "java.lang.Boolean"; - } else if ("int".equals(type) || "integer".equals(type)) { - type = "java.lang.Integer"; - } else if ("byte".equals(type)) { - type = "java.lang.Byte"; - } else if ("short".equals(type)) { - type = "java.lang.Short"; - } else if ("double".equals(type)) { - type = "java.lang.Double"; - } else if ("float".equals(type)) { - type = "java.lang.Float"; - } + type = getSimpleJavaType(type); PropertySource<JavaClassSource> prop = javaClass.addProperty(type, option.getName()); if ("true".equals(option.getDeprecated())) { @@ -480,24 +639,8 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { } } // CHECKSTYLE:ON - - // remove <?> as generic type as Roaster (Eclipse JDT) cannot use that String type = option.getJavaType(); - type = type.replaceAll("\\<\\?\\>", ""); - // use wrapper types for primitive types so a null mean that the option has not been configured - if ("boolean".equals(type)) { - type = "java.lang.Boolean"; - } else if ("int".equals(type) || "integer".equals(type)) { - type = "java.lang.Integer"; - } else if ("byte".equals(type)) { - type = "java.lang.Byte"; - } else if ("short".equals(type)) { - type = "java.lang.Short"; - } else if ("double".equals(type)) { - type = "java.lang.Double"; - } else if ("float".equals(type)) { - type = "java.lang.Float"; - } + type = getSimpleJavaType(type); PropertySource<JavaClassSource> prop = javaClass.addProperty(type, option.getName()); if ("true".equals(option.getDeprecated())) { @@ -724,6 +867,23 @@ public class SpringBootAutoConfigurationMojo extends AbstractMojo { sb.append("Map<String, Object> parameters = new HashMap<>();\n"); sb.append("IntrospectionSupport.getProperties(configuration, parameters, null, false);\n"); sb.append("\n"); + sb.append("for (Map.Entry<String, Object> entry : parameters.entrySet()) {\n"); + sb.append(" Object value = entry.getValue();\n"); + sb.append(" Class<?> paramClass = value.getClass();\n"); + sb.append(" if (paramClass.getName().endsWith(\"NestedConfiguration\")) {\n"); + sb.append(" Class nestedClass = null;\n"); + sb.append(" try {\n"); + sb.append(" nestedClass = (Class) paramClass.getDeclaredField(\"CAMEL_NESTED_CLASS\").get(null);\n"); + sb.append(" HashMap<String, Object> nestedParameters = new HashMap<>();\n"); + sb.append(" IntrospectionSupport.getProperties(value, nestedParameters, null, false);\n"); + sb.append(" Object nestedProperty = nestedClass.newInstance();\n"); + sb.append(" IntrospectionSupport.setProperties(camelContext, camelContext.getTypeConverter(), nestedProperty, nestedParameters);\n"); + sb.append(" entry.setValue(nestedProperty);\n"); + sb.append(" } catch (NoSuchFieldException e) {\n"); + sb.append(" // ignore, class must not be a nested configuration class after all\n"); + sb.append(" }\n"); + sb.append(" }\n"); + sb.append("}\n"); sb.append("IntrospectionSupport.setProperties(camelContext, camelContext.getTypeConverter(), component, parameters);\n"); sb.append("\n"); sb.append("return component;");