blob: c76cf89ede701895d7e1d3a904051d67e326c7a5 [file] [log] [blame]
/*
* 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.camel.maven.model.AutowireData;
import org.apache.camel.maven.model.SpringBootGroupData;
import org.apache.camel.maven.model.SpringBootPropertyData;
import org.apache.camel.support.IntrospectionSupport;
import org.apache.camel.support.PatternHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.OrderedProperties;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.json.Jsoner;
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.Extendable;
import org.jboss.forge.roaster.model.JavaType;
import org.jboss.forge.roaster.model.source.Importer;
import org.jboss.forge.roaster.model.source.JavaClassSource;
import org.jboss.forge.roaster.model.source.MethodHolderSource;
import org.jboss.forge.roaster.model.source.MethodSource;
import static org.apache.camel.maven.GenerateHelper.sanitizeDescription;
import static org.apache.camel.util.StringHelper.camelCaseToDash;
/**
* Pre scans your project and prepares autowiring and spring-boot tooling support by classpath scanning.
*/
@Mojo(name = "generate", defaultPhase = LifecyclePhase.PROCESS_CLASSES, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE)
public class GenerateMojo extends AbstractMainMojo {
/**
* Whether generating autowiring is enabled.
*/
@Parameter(property = "camel.autowireEnabled", defaultValue = "true")
protected boolean autowireEnabled;
/**
* Whether generating spring boot tooling support is enabled.
*/
@Parameter(property = "camel.springBootEnabled", defaultValue = "true")
protected boolean springBootEnabled;
/**
* Whether to allow downloading -source JARs when generating spring boot tooling to include
* javadoc as description for discovered options.
*/
@Parameter(property = "camel.downloadSourceJars", defaultValue = "true")
protected boolean downloadSourceJars;
/**
* When autowiring has detected multiple implementations (2 or more) of a given interface, which
* cannot be mapped, should they be logged so you can see and add manual mapping if needed.
*/
@Parameter(property = "camel.logUnmapped", defaultValue = "false")
protected boolean logUnmapped;
/**
* The output directory for generated autowire file
*/
@Parameter(readonly = true, defaultValue = "${project.build.directory}/classes/META-INF/services/org/apache/camel/")
protected File outAutowireFolder;
/**
* The output directory for generated spring boot tooling file
*/
@Parameter(readonly = true, defaultValue = "${project.build.directory}/../src/main/resources/META-INF/")
protected File outSpringBootFolder;
/**
* To exclude autowiring specific properties with these key names.
* You can also configure a single entry and separate the excludes with comma
*/
@Parameter(property = "camel.exclude")
protected String[] exclude;
/**
* To include autowiring specific properties or component with these key names.
* You can also configure a single entry and separate the includes with comma
*/
@Parameter(property = "camel.include")
protected String[] include;
/**
* To setup special mappings between known types as key=value pairs.
* You can also configure a single entry and separate the mappings with comma
*/
@Parameter(property = "camel.mappings")
protected String[] mappings;
/**
* Optional mappings file loaded from classpath, with mapping that override any default mappings
*/
@Parameter(defaultValue = "${project.build.directory}/classes/camel-main-mappings.properties")
protected File mappingsFile;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
// load default mappings
Properties mappingProperties = loadDefaultMappings();
getLog().debug("Loaded default-mappings: " + mappingProperties);
// add extra mappings
if (this.mappings != null) {
for (String m : this.mappings) {
String key = StringHelper.before(m, "=");
String value = StringHelper.after(m, "=");
if (key != null && value != null) {
mappingProperties.setProperty(key, value);
getLog().debug("Added mapping from pom.xml: " + key + "=" + value);
}
}
}
Properties mappingFileProperties = loadMappingsFile();
if (!mappingFileProperties.isEmpty()) {
getLog().debug("Loaded mappings file: " + mappingsFile + " with mappings: " + mappingFileProperties);
mappingProperties.putAll(mappingFileProperties);
}
final List<AutowireData> autowireData = new ArrayList<>();
final List<SpringBootPropertyData> propertyData = new ArrayList<>();
final List<SpringBootGroupData> groupData = new ArrayList<>();
String existingText = null;
if (springBootEnabled) {
// is this a new component
File file = new File(outSpringBootFolder, "spring-configuration-metadata.json");
if (file.exists()) {
try {
InputStream is = new FileInputStream(file);
existingText = IOHelper.loadText(is);
IOHelper.close(is);
} catch (FileNotFoundException e) {
// ignore
} catch (IOException e) {
throw new MojoExecutionException("Error loading " + outSpringBootFolder + "/spring-configuration-metadata.json file");
}
}
}
final String springBootExistingText = existingText;
final AtomicBoolean springBootChanged = new AtomicBoolean(existingText == null);
ComponentCallback callback = (componentName, componentJavaType, componentDescription,
name, type, javaType, description, defaultValue, deprecated) -> {
// gather spring boot data
// we want to use dash in the name
String dash = camelCaseToDash(name);
String key = "camel.component." + componentName + "." + dash;
if (springBootEnabled) {
getLog().debug("Spring Boot option: " + key);
propertyData.add(new SpringBootPropertyData(key, springBootJavaType(javaType), description, componentJavaType, defaultValue, deprecated));
// avoid duplicate groups (it has equal/hashCode contract on name only)
SpringBootGroupData group = new SpringBootGroupData("camel.component." + componentName, componentDescription, componentJavaType);
if (!groupData.contains(group)) {
groupData.add(group);
}
if (springBootExistingText != null && !springBootExistingText.contains(group.getName())) {
springBootChanged.set(true);
}
}
// check if we can do automatic autowire to complex singleton objects from classes in the classpath
if (autowireEnabled && "object".equals(type)) {
if (!isValidAutowirePropertyName(componentName, name)) {
getLog().debug("Skipping property name: " + name);
return;
}
try {
Class clazz = classLoader.loadClass(javaType);
if (clazz.isInterface() && isComplexUserType(clazz)) {
Set<Class<?>> classes = reflections.getSubTypesOf(clazz);
// filter classes (must not be interfaces, must be public, must not be abstract, must be top level) and also a valid autowire class
classes = classes.stream().filter(
c -> !c.isInterface()
&& Modifier.isPublic(c.getModifiers())
&& !Modifier.isAbstract(c.getModifiers())
&& c.getEnclosingClass() == null
&& isValidAutowireClass(c))
.collect(Collectors.toSet());
Class best = chooseBestKnownType(componentName, name, clazz, classes, mappingProperties);
if (best != null) {
key = "camel.component." + componentName + "." + dash;
String value = "#class:" + best.getName();
getLog().debug("Autowire: " + key + "=" + value);
autowireData.add(new AutowireData(key, value));
if (springBootEnabled && springBootChanged.get()) {
// gather additional spring boot data for this class
// we dont have documentation or default values
List<Method> setters = new ArrayList<>();
extraSetterMethods(best, setters);
// sort the setters
setters.sort((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
JavaClassSource javaClassSource = null;
if (downloadSourceJars && !setters.isEmpty()) {
String path = best.getName().replace('.', '/') + ".java";
getLog().debug("Loading Java source: " + path);
InputStream is = getSourcesClassLoader().getResourceAsStream(path);
if (is != null) {
try {
String text = IOHelper.loadText(is);
IOHelper.close(is);
javaClassSource = (JavaClassSource) Roaster.parse(text);
} catch (IOException e) {
// ignore
getLog().warn("Cannot load Java source: " + path + " from classpath due " + e.getMessage());
}
}
getLog().debug("Loaded source code: " + clazz);
}
for (Method m : setters) {
String shortHand = IntrospectionSupport.getSetterShorthandName(m);
String bootName = camelCaseToDash(shortHand);
String bootKey = "camel.component." + componentName + "." + dash + "." + bootName;
String bootJavaType = m.getParameterTypes()[0].getName();
String sourceType = best.getName();
boolean bootDeprecated = m.getAnnotation(Deprecated.class) != null;
getLog().debug("Spring Boot option: " + bootKey);
// find the setter method and grab the javadoc
String desc = extractJavaDocFromMethod(javaClassSource, m);
desc = sanitizeDescription(desc, false);
if (desc == null) {
desc = "";
} else {
if (desc.endsWith(".")) {
desc += " ";
} else {
desc += ". ";
}
}
desc += "Auto discovered option from class: " + best.getName() + " to set the option via setter: " + m.getName();
propertyData.add(new SpringBootPropertyData(bootKey, springBootJavaType(bootJavaType), desc, sourceType, null, bootDeprecated));
}
}
}
}
} catch (Exception e) {
// ignore
getLog().debug("Cannot load class: " + name, e);
}
}
};
// execute with the callback
doExecute(callback);
// write the output files
writeAutowireFile(autowireData);
if (springBootChanged.get()) {
writeSpringBootFile(propertyData, groupData);
}
}
protected void writeSpringBootFile(List<SpringBootPropertyData> propertyData, List<SpringBootGroupData> groupData) throws MojoFailureException {
if (!propertyData.isEmpty()) {
List options = new ArrayList();
for (SpringBootPropertyData row : propertyData) {
Map p = new LinkedHashMap();
p.put("name", row.getName());
p.put("type", row.getJavaType());
p.put("sourceType", row.getSourceType());
p.put("description", row.getDescription());
if (row.getDefaultValue() != null) {
p.put("defaultValue", row.getDefaultValue());
}
if (row.isDeprecated()) {
p.put("deprecated", true);
p.put("deprecation", Collections.EMPTY_MAP);
}
options.add(p);
}
// okay then add the components into the main json at the end so they get merged together
// load camel-main metadata
String mainJson = loadCamelMainConfigurationMetadata();
if (mainJson == null) {
getLog().warn("Cannot load camel-main-configuration-metadata.json from within the camel-main JAR from the classpath."
+ " Not possible to build spring boot configuration file for this project");
return;
}
String json;
try {
Map map = (Map) Jsoner.deserialize(mainJson);
List props = (List) map.get("properties");
props.addAll(options);
// include groups
List groups = (List) map.get("groups");
groupData.forEach(g -> {
Map group = new LinkedHashMap();
group.put("name", g.getName());
group.put("description", g.getDescription());
group.put("sourceType", g.getSourceType());
groups.add(group);
});
json = Jsoner.serialize(map);
json = Jsoner.prettyPrint(json);
} catch (Throwable e) {
throw new MojoFailureException("Cannot serialize or deserialize json due " + e.getMessage(), e);
}
outSpringBootFolder.mkdirs();
File file = new File(outSpringBootFolder, "spring-configuration-metadata.json");
try {
FileOutputStream fos = new FileOutputStream(file, false);
fos.write(json.getBytes());
IOHelper.close(fos);
getLog().info("Created file: " + file);
} catch (Throwable e) {
throw new MojoFailureException("Cannot write to file " + file + " due " + e.getMessage(), e);
}
}
}
protected void writeAutowireFile(List<AutowireData> autowireData) throws MojoFailureException {
if (!autowireData.isEmpty()) {
outAutowireFolder.mkdirs();
File file = new File(outAutowireFolder, "autowire.properties");
String existingText = null;
if (file.exists()) {
try {
InputStream is = new FileInputStream(file);
existingText = IOHelper.loadText(is);
IOHelper.close(is);
} catch (Exception e) {
// ignore
}
}
final String autowireExisting = existingText;
// only write the file if its changed
boolean matches = autowireExisting != null && autowireData.stream().allMatch(d -> autowireExisting.contains(d.getKey() + "=" + d.getValue()));
if (!matches) {
try {
FileOutputStream fos = new FileOutputStream(file, false);
fos.write("# Generated by camel build tools\n".getBytes());
for (AutowireData data : autowireData) {
fos.write(data.getKey().getBytes());
fos.write('=');
fos.write(data.getValue().getBytes());
fos.write('\n');
}
IOHelper.close(fos);
getLog().info("Created file: " + file + " (autowire by classpath: " + autowireData.size() + ")");
} catch (Throwable e) {
throw new MojoFailureException("Cannot write to file " + file + " due " + e.getMessage(), e);
}
}
}
}
protected Properties loadDefaultMappings() throws MojoFailureException {
Properties mappings = new OrderedProperties();
try {
InputStream is = GenerateMojo.class.getResourceAsStream("/default-mappings.properties");
if (is != null) {
mappings.load(is);
}
} catch (IOException e) {
throw new MojoFailureException("Cannot load default-mappings.properties from classpath");
}
return mappings;
}
protected Properties loadMappingsFile() throws MojoFailureException {
Properties mappings = new OrderedProperties();
if (mappingsFile.exists() && mappingsFile.isFile()) {
try {
InputStream is = new FileInputStream(mappingsFile);
mappings.load(is);
} catch (IOException e) {
throw new MojoFailureException("Cannot load file: " + mappingsFile);
}
}
return mappings;
}
protected Class chooseBestKnownType(String componentName, String optionName, Class type, Set<Class<?>> candidates, Properties knownTypes) {
String known = knownTypes.getProperty(type.getName());
if (known != null) {
for (String k : known.split(";")) {
// special as we should skip this option
if ("#skip#".equals(k)) {
return null;
}
// jump to after the leading prefix to have the classname
if (k.startsWith("#class:")) {
k = k.substring(7);
} else if (k.startsWith("#type:")) {
k = k.substring(6);
}
final String candiateName = k;
Class found = candidates.stream().filter(c -> c.getName().equals(candiateName)).findFirst().orElse(null);
if (found != null) {
return found;
}
}
}
if (candidates.size() == 1) {
return candidates.iterator().next();
} else if (candidates.size() > 1) {
if (logUnmapped) {
getLog().debug("Cannot chose best type: " + type.getName() + " among " + candidates.size() + " implementations: " + candidates);
getLog().info("Cannot autowire option camel.component." + componentName + "." + optionName
+ " as the interface: " + type.getName() + " has " + candidates.size() + " implementations in the classpath:");
for (Class c : candidates) {
getLog().info("\t\t" + c.getName());
}
}
}
return null;
}
protected boolean isValidAutowirePropertyName(String componentName, String name) {
// we want to regard names as the same if they are using dash or not, and also to be case insensitive.
String prefix = "camel.component." + componentName + ".";
name = StringHelper.dashToCamelCase(name);
if (exclude != null && exclude.length > 0) {
// works on components too
for (String pattern : exclude) {
pattern = pattern.trim();
pattern = StringHelper.dashToCamelCase(pattern);
if (PatternHelper.matchPattern(componentName, pattern)) {
return false;
}
if (PatternHelper.matchPattern(name, pattern) || PatternHelper.matchPattern(prefix + name, pattern)) {
return false;
}
}
}
if (include != null && include.length > 0) {
for (String pattern : include) {
pattern = pattern.trim();
pattern = StringHelper.dashToCamelCase(pattern);
if (PatternHelper.matchPattern(componentName, pattern)) {
return true;
}
if (PatternHelper.matchPattern(name, pattern) || PatternHelper.matchPattern(prefix + name, pattern)) {
return true;
}
}
// we have include enabled and none matched so it should be false
return false;
}
return true;
}
private static boolean isComplexUserType(Class type) {
// lets consider all non java, as complex types
return type != null && !type.isPrimitive() && !type.getName().startsWith("java.");
}
private static boolean isValidAutowireClass(Class clazz) {
// skip all from Apache Camel and regular JDK as they would be default anyway
return !clazz.getName().startsWith("org.apache.camel");
}
private String loadCamelMainConfigurationMetadata() throws MojoFailureException {
try {
InputStream is = classLoader.getResourceAsStream("META-INF/camel-main-configuration-metadata.json");
String text = IOHelper.loadText(is);
IOHelper.close(is);
return text;
} catch (Throwable e) {
throw new MojoFailureException("Error during discovering camel-main from classpath due " + e.getMessage(), e);
}
}
private static String springBootJavaType(String javaType) {
if ("boolean".equalsIgnoreCase(javaType)) {
return "java.lang.Boolean";
} else if ("int".equalsIgnoreCase(javaType)) {
return "java.lang.Integer";
} else if ("long".equalsIgnoreCase(javaType)) {
return "java.lang.Long";
} else if ("string".equalsIgnoreCase(javaType)) {
return "java.lang.String";
}
return javaType;
}
private static boolean springBootDefaultValueQuotes(String javaType) {
if ("java.lang.Boolean".equalsIgnoreCase(javaType)) {
return false;
} else if ("java.lang.Integer".equalsIgnoreCase(javaType)) {
return false;
} else if ("java.lang.Long".equalsIgnoreCase(javaType)) {
return false;
}
return true;
}
private static void extraSetterMethods(Class<?> clazz, List<Method> answer) {
if (clazz == null || clazz == Object.class) {
return;
}
Method[] methods = clazz.getMethods();
for (Method m : methods) {
if (IntrospectionSupport.isSetter(m)) {
answer.add(m);
}
}
}
private String extractJavaDocFromMethod(Object input, Method method) {
if (input == null) {
return null;
}
String answer = "";
// input can be both a class or interface so we need to split this up into several pieces
MethodHolderSource mhs = (MethodHolderSource) input;
JavaType jt = (JavaType) input;
Extendable ext = null;
if (input instanceof Extendable) {
ext = (Extendable) input;
}
Importer importer = null;
if (input instanceof Importer) {
importer = (Importer) input;
}
MethodSource ms = mhs.getMethod(method.getName(), method.getParameterTypes()[0]);
if (ms != null) {
answer = ms.getJavaDoc().getText();
}
String st = null;
if (answer.isEmpty()) {
// special for activemq-artemis
if ("org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory".equals(jt.getQualifiedName())) {
st = "org.apache.activemq.artemis.api.core.client.ServerLocator";
} else {
// maybe its from the super class
st = ext != null ? ext.getSuperType() : null;
}
}
if (answer.isEmpty() && st != null && !"java.lang.Object".equals(st) && !"Object".equals(st)) {
st = importer != null ? importer.resolveType(st) : st;
// find this file cia classloader
String path = st.replace('.', '/') + ".java";
InputStream is = sourcesClassLoader.getResourceAsStream(path);
if (is != null) {
String text;
try {
text = IOHelper.loadText(is);
IOHelper.close(is);
input = Roaster.parse(text);
getLog().debug("Loaded source code: " + input);
answer = extractJavaDocFromMethod(input, method);
} catch (IOException e) {
// ignore
getLog().warn("Cannot load Java source: " + path + " from classpath due " + e.getMessage());
}
}
}
return answer;
}
}