/** * 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.connector; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.annotation.Generated; import javax.annotation.PostConstruct; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.camel.maven.connector.model.ComponentModel; import org.apache.camel.maven.connector.model.ComponentOptionModel; import org.apache.camel.maven.connector.model.EndpointOptionModel; import org.apache.camel.maven.connector.model.OptionModel; import org.apache.camel.maven.connector.util.JSonSchemaHelper; import org.apache.commons.io.FileUtils; import org.apache.maven.plugin.AbstractMojo; 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.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; import org.jboss.forge.roaster.model.source.Import; import org.jboss.forge.roaster.model.source.JavaClassSource; import org.jboss.forge.roaster.model.source.MethodSource; import org.jboss.forge.roaster.model.source.PropertySource; import org.jboss.forge.roaster.model.util.Formatter; import org.jboss.forge.roaster.model.util.Strings; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import static org.apache.camel.maven.connector.util.FileHelper.loadText; import static org.apache.camel.maven.connector.util.StringHelper.getSafeValue; import static org.apache.camel.maven.connector.util.StringHelper.getShortJavaType; /** * Generate Spring Boot auto configuration files for Camel connectors. */ @Mojo(name = "prepare-spring-boot-auto-configuration", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true) public class SpringBootAutoConfigurationMojo extends AbstractMojo { @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) private File classesDirectory; @Parameter(defaultValue = "true") private boolean includeLicenseHeader; @Parameter(defaultValue = "camel.connector") private String configurationPrefix; @Override public void execute() throws MojoExecutionException, MojoFailureException { try { executeConnector(); } catch (Exception e) { throw new MojoFailureException("Error generating Spring-Boot auto configuration for connector", e); } } @SuppressWarnings("unchecked") private void executeConnector() throws Exception { String javaType = null; String connectorScheme = null; List<String> componentOptions = Collections.emptyList(); List<String> endpointOptions = Collections.emptyList(); File file = new File(classesDirectory, "camel-connector.json"); if (file.exists()) { ObjectMapper mapper = new ObjectMapper(); Map dto = mapper.readValue(file, Map.class); javaType = (String) dto.get("javaType"); connectorScheme = (String) dto.get("scheme"); componentOptions = (List) dto.get("componentOptions"); endpointOptions = (List) dto.get("endpointOptions"); } // find the component dependency and get its .json file file = new File(classesDirectory, "camel-component-schema.json"); if (file.exists() && javaType != null && connectorScheme != null) { String json = loadText(new FileInputStream(file)); ComponentModel model = generateComponentModel(json); // resolvePropertyPlaceholders is an option which only make sense to use if the component has other options boolean hasOptions = model.getComponentOptions().stream().anyMatch(o -> !o.getName().equals("resolvePropertyPlaceholders")); // use springboot as sub package name so the code is not in normal // package so the Spring Boot JARs can be optional at runtime int pos = javaType.lastIndexOf("."); String pkg = javaType.substring(0, pos) + ".springboot"; // we only create spring boot auto configuration if there is options to configure if (hasOptions) { getLog().info("Generating Spring Boot AutoConfiguration for Connector: " + model.getScheme()); createConnectorConfigurationSource(pkg, model, javaType, connectorScheme, componentOptions, endpointOptions); createConnectorAutoConfigurationSource(pkg, hasOptions, javaType, connectorScheme); createConnectorSpringFactorySource(pkg, javaType); } } } private void createConnectorSpringFactorySource(String packageName, String javaType) throws MojoFailureException { int pos = javaType.lastIndexOf("."); String name = javaType.substring(pos + 1); name = name.replace("Component", "ConnectorAutoConfiguration"); writeComponentSpringFactorySource(packageName, name); } private void writeComponentSpringFactorySource(String packageName, String name) throws MojoFailureException { StringBuilder sb = new StringBuilder(); sb.append("org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\n"); String lineToAdd = packageName + "." + name + "\n"; sb.append(lineToAdd); // project root folder File root = classesDirectory.getParentFile().getParentFile(); String fileName = "src/main/resources/META-INF/spring.factories"; File target = new File(root, fileName); // create new file try { String header = ""; if (includeLicenseHeader) { InputStream is = getClass().getClassLoader().getResourceAsStream("license-header.txt"); header = loadText(is); } String code = sb.toString(); // add empty new line after header code = header + "\n" + code; getLog().debug("Source code generated:\n" + code); FileUtils.write(target, code); getLog().info("Created file: " + target); } catch (Exception e) { throw new MojoFailureException("IOError with file " + target, e); } } private void createConnectorConfigurationSource(String packageName, ComponentModel model, String javaType, String connectorScheme, List<String> componentOptions, List<String> endpointOptions) throws MojoFailureException { final int pos = javaType.lastIndexOf("."); final String commonName = javaType.substring(pos + 1).replace("Component", "ConnectorConfigurationCommon"); final String configName = javaType.substring(pos + 1).replace("Component", "ConnectorConfiguration"); // Common base class JavaClassSource commonClass = Roaster.create(JavaClassSource.class); commonClass.setPackage(packageName); commonClass.setName(commonName); String doc = "Generated by camel-package-maven-plugin - do not edit this file!"; if (!Strings.isBlank(model.getDescription())) { doc = model.getDescription() + "\n\n" + doc; } // replace Component with Connector doc = doc.replaceAll("Component", "Connector"); doc = doc.replaceAll("component", "connector"); commonClass.getJavaDoc().setFullText(doc); commonClass.addAnnotation(Generated.class).setStringValue("value", SpringBootAutoConfigurationMojo.class.getName()); // compute the configuration prefix to use with spring boot configuration String prefix = ""; if (!"false".equalsIgnoreCase(configurationPrefix)) { // make sure prefix is in lower case prefix = configurationPrefix.toLowerCase(Locale.US); if (!prefix.endsWith(".")) { prefix += "."; } } prefix += connectorScheme.toLowerCase(Locale.US); for (OptionModel option : model.getComponentOptions()) { boolean isComponentOption= componentOptions != null && componentOptions.stream().anyMatch(o -> o.equals(option.getName())); boolean isEndpointOption = endpointOptions != null && endpointOptions.stream().anyMatch(o -> o.equals(option.getName())); // only include the options that has been explicit configured in the // componentOptions section of camel-connector.json file and exclude // those configured on endpointOptions in the same file if (isComponentOption && !isEndpointOption) { addProperty(commonClass, model, option); } } for (OptionModel option : model.getEndpointOptions()) { if (endpointOptions != null && endpointOptions.stream().anyMatch(o -> o.equals(option.getName()))) { addProperty(commonClass, model, option); } } sortImports(commonClass); writeSourceIfChanged(commonClass, packageName.replaceAll("\\.", "\\/") + "/" + commonName + ".java"); // Config class JavaClassSource configClass = Roaster.create(JavaClassSource.class); configClass.setPackage(packageName); configClass.setName(configName); configClass.extendSuperType(commonClass); configClass.addAnnotation(Generated.class).setStringValue("value", SpringBootAutoConfigurationMojo.class.getName()); configClass.addAnnotation(ConfigurationProperties.class).setStringValue("prefix", prefix); configClass.addImport(Map.class); configClass.addImport(HashMap.class); configClass.removeImport(commonClass); configClass.addField("Map<String, " + commonName + "> configurations = new HashMap<>()") .setPrivate() .getJavaDoc().setFullText("Define additional configuration definitions"); MethodSource<JavaClassSource> method; method = configClass.addMethod(); method.setName("getConfigurations"); method.setReturnType("Map<String, " + commonName + ">"); method.setPublic(); method.setBody("return configurations;"); sortImports(configClass); writeSourceIfChanged(configClass, packageName.replaceAll("\\.", "\\/") + "/" + configName + ".java"); } private void createConnectorAutoConfigurationSource(String packageName, boolean hasOptions, String javaType, String connectorScheme) throws MojoFailureException { final JavaClassSource javaClass = Roaster.create(JavaClassSource.class); int pos = javaType.lastIndexOf("."); String name = javaType.substring(pos + 1); name = name.replace("Component", "ConnectorAutoConfiguration"); final String configNameCommon = javaType.substring(pos + 1).replace("Component", "ConnectorConfigurationCommon"); final String configName = javaType.substring(pos + 1).replace("Component", "ConnectorConfiguration"); javaClass.setPackage(packageName).setName(name); String doc = "Generated by camel-connector-maven-plugin - do not edit this file!"; javaClass.getJavaDoc().setFullText(doc); javaClass.addAnnotation(Generated.class).setStringValue("value", SpringBootAutoConfigurationMojo.class.getName()); javaClass.addAnnotation(Configuration.class); javaClass.addAnnotation(ConditionalOnBean.class).setStringValue("type", "org.apache.camel.spring.boot.CamelAutoConfiguration"); javaClass.addAnnotation(AutoConfigureAfter.class).setStringValue("name", "org.apache.camel.spring.boot.CamelAutoConfiguration"); String configurationName = name.replace("ConnectorAutoConfiguration", "ConnectorConfiguration"); if (hasOptions) { AnnotationSource<JavaClassSource> ann = javaClass.addAnnotation(EnableConfigurationProperties.class); ann.setLiteralValue("value", configurationName + ".class"); javaClass.addImport("java.util.HashMap"); javaClass.addImport("java.util.Map"); javaClass.addImport("org.apache.camel.util.IntrospectionSupport"); } javaClass.addImport(javaType); javaClass.addImport(BeanCreationException.class); javaClass.addImport("org.apache.camel.CamelContext"); javaClass.addField() .setName("camelContext") .setType("org.apache.camel.CamelContext") .setPrivate() .addAnnotation(Autowired.class); javaClass.addField() .setName("configuration") .setType(configName) .setPrivate() .addAnnotation(Autowired.class); // add method for auto configure String shortJavaType = getShortJavaType(javaType); // must be named -component because camel-spring-boot uses that to lookup components String beanName = connectorScheme + "-component"; MethodSource<JavaClassSource> configureMethod = javaClass.addMethod() .setName("configure" + shortJavaType) .setPublic() .setBody(createComponentBody(shortJavaType, hasOptions)) .setReturnType(shortJavaType) .addThrows(Exception.class); configureMethod.addAnnotation(Lazy.class); configureMethod.addAnnotation(Bean.class).setStringValue("name", beanName); configureMethod.addAnnotation(ConditionalOnClass.class).setLiteralValue("value", "CamelContext.class"); configureMethod.addAnnotation(ConditionalOnMissingBean.class).setStringValue("name", beanName); MethodSource<JavaClassSource> postProcessMethod = javaClass.addMethod() .setName("postConstruct" + shortJavaType) .setPublic() .setBody(createPostConstructBody(shortJavaType, configNameCommon)); postProcessMethod.addAnnotation(PostConstruct.class); sortImports(javaClass); String fileName = packageName.replaceAll("\\.", "\\/") + "/" + name + ".java"; writeSourceIfChanged(javaClass, fileName); } private void writeSourceIfChanged(JavaClassSource source, String fileName) throws MojoFailureException { // project root folder File root = classesDirectory.getParentFile().getParentFile(); File target = new File(root, "src/main/java/" + fileName); try { String header = ""; if (includeLicenseHeader) { InputStream is = getClass().getClassLoader().getResourceAsStream("license-header-java.txt"); header = loadText(is); } String code = sourceToString(source); code = header + code; getLog().debug("Source code generated:\n" + code); if (target.exists()) { String existing = FileUtils.readFileToString(target); if (!code.equals(existing)) { FileUtils.write(target, code, false); getLog().info("Updated existing file: " + target); } else { getLog().debug("No changes to existing file: " + target); } } else { FileUtils.write(target, code); getLog().info("Created file: " + target); } } catch (Exception e) { throw new MojoFailureException("IOError with file " + target, e); } } private static String createComponentBody(String shortJavaType, boolean hasOptions) { StringBuilder sb = new StringBuilder(); sb.append(shortJavaType).append(" connector = new ").append(shortJavaType).append("();").append("\n"); sb.append("connector.setCamelContext(camelContext);\n"); sb.append("\n"); if (hasOptions) { sb.append("Map<String, Object> parameters = new HashMap<>();\n"); sb.append("IntrospectionSupport.getProperties(configuration, parameters, null, false);\n"); sb.append("IntrospectionSupport.setProperties(camelContext, camelContext.getTypeConverter(), connector, parameters);\n"); sb.append("connector.setComponentOptions(parameters);\n"); } sb.append("\n"); sb.append("return connector;"); return sb.toString(); } private static String createPostConstructBody(String shortJavaType, String commonConfigurationName) { StringBuilder sb = new StringBuilder(); sb.append("if (camelContext != null) {\n"); sb.append("Map<String, Object> parameters = new HashMap<>();\n"); sb.append("\n"); sb.append("for (Map.Entry<String, " + commonConfigurationName + "> entry : configuration.getConfigurations().entrySet()) {\n"); sb.append("parameters.clear();\n"); sb.append("\n"); sb.append(shortJavaType).append(" connector = new ").append(shortJavaType).append("();\n"); sb.append("connector.setCamelContext(camelContext);\n"); sb.append("\n"); sb.append("try {\n"); sb.append("IntrospectionSupport.getProperties(entry.getValue(), parameters, null, false);\n"); sb.append("IntrospectionSupport.setProperties(camelContext, camelContext.getTypeConverter(), connector, parameters);\n"); sb.append("connector.setComponentOptions(parameters);\n"); sb.append("\n"); sb.append("camelContext.addComponent(entry.getKey(), connector);\n"); sb.append("} catch (Exception e) {\n"); sb.append("throw new BeanCreationException(entry.getKey(), e.getMessage(), e);\n"); sb.append("}\n"); sb.append("}\n"); sb.append("}\n"); return sb.toString(); } private static void sortImports(JavaClassSource javaClass) { // sort imports List<Import> imports = javaClass.getImports(); // sort imports List<String> names = new ArrayList<>(); for (Import imp : imports) { names.add(imp.getQualifiedName()); } // sort Collections.sort(names, (s1, s2) -> { // java comes first if (s1.startsWith("java.")) { s1 = "___" + s1; } if (s2.startsWith("java.")) { s2 = "___" + s2; } // then javax comes next if (s1.startsWith("javax.")) { s1 = "__" + s1; } if (s2.startsWith("javax.")) { s2 = "__" + s2; } // org.w3c is for some odd reason also before others if (s1.startsWith("org.w3c.")) { s1 = "_" + s1; } if (s2.startsWith("org.w3c.")) { s2 = "_" + s2; } return s1.compareTo(s2); }); // remove all imports first for (String name : names) { javaClass.removeImport(name); } // and add them back in correct order for (String name : names) { javaClass.addImport(name); } } private static String sourceToString(JavaClassSource javaClass) { String code = Formatter.format(javaClass); // convert tabs to 4 spaces code = code.replaceAll("\\t", " "); return code; } private static ComponentModel generateComponentModel(String json) { List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false); ComponentModel component = new ComponentModel(); component.setScheme(getSafeValue("scheme", rows)); component.setSyntax(getSafeValue("syntax", rows)); component.setAlternativeSyntax(getSafeValue("alternativeSyntax", rows)); component.setTitle(getSafeValue("title", rows)); component.setDescription(getSafeValue("description", rows)); component.setFirstVersion(getSafeValue("firstVersion", rows)); component.setLabel(getSafeValue("label", rows)); component.setDeprecated(getSafeValue("deprecated", rows)); component.setConsumerOnly(getSafeValue("consumerOnly", rows)); component.setProducerOnly(getSafeValue("producerOnly", rows)); component.setJavaType(getSafeValue("javaType", rows)); component.setGroupId(getSafeValue("groupId", rows)); component.setArtifactId(getSafeValue("artifactId", rows)); component.setVersion(getSafeValue("version", rows)); rows = JSonSchemaHelper.parseJsonSchema("componentProperties", json, true); for (Map<String, String> row : rows) { ComponentOptionModel option = new ComponentOptionModel(); option.setName(getSafeValue("name", row)); option.setDisplayName(getSafeValue("displayName", row)); option.setKind(getSafeValue("kind", row)); option.setType(getSafeValue("type", row)); option.setJavaType(getSafeValue("javaType", row)); option.setDeprecated(getSafeValue("deprecated", row)); option.setDescription(getSafeValue("description", row)); option.setDefaultValue(getSafeValue("defaultValue", row)); option.setEnums(getSafeValue("enum", row)); component.addComponentOption(option); } rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); for (Map<String, String> row : rows) { EndpointOptionModel option = new EndpointOptionModel(); option.setName(getSafeValue("name", row)); option.setDisplayName(getSafeValue("displayName", row)); option.setKind(getSafeValue("kind", row)); option.setGroup(getSafeValue("group", row)); option.setRequired(getSafeValue("required", row)); option.setType(getSafeValue("type", row)); option.setJavaType(getSafeValue("javaType", row)); option.setEnums(getSafeValue("enum", row)); option.setPrefix(getSafeValue("prefix", row)); option.setMultiValue(getSafeValue("multiValue", row)); option.setDeprecated(getSafeValue("deprecated", row)); option.setDefaultValue(getSafeValue("defaultValue", row)); option.setDescription(getSafeValue("description", row)); option.setEnumValues(getSafeValue("enum", row)); component.addEndpointOption(option); } return component; } private void addProperty(JavaClassSource clazz, ComponentModel model, OptionModel option) { String type = option.getJavaType(); PropertySource<JavaClassSource> prop = clazz.addProperty(type, option.getName()); if ("true".equals(option.getDeprecated())) { 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); } if (!Strings.isBlank(option.getDescription())) { prop.getField().getJavaDoc().setFullText(option.getDescription()); } if (!Strings.isBlank(option.getDefaultValue())) { if ("java.lang.String".equals(option.getJavaType())) { prop.getField().setStringInitializer(option.getDefaultValue()); } else if ("long".equals(option.getJavaType()) || "java.lang.Long".equals(option.getJavaType())) { // the value should be a Long number String value = option.getDefaultValue() + "L"; prop.getField().setLiteralInitializer(value); } else if ("integer".equals(option.getType()) || "boolean".equals(option.getType())) { prop.getField().setLiteralInitializer(option.getDefaultValue()); } else if (!Strings.isBlank(option.getEnums())) { String enumShortName = type.substring(type.lastIndexOf(".") + 1); prop.getField().setLiteralInitializer(enumShortName + "." + option.getDefaultValue()); clazz.addImport(model.getJavaType()); } } } }