/**
* 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.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.camel.util.IntrospectionSupport;
import org.apache.camel.util.component.ApiCollection;
import org.apache.camel.util.component.ApiMethod;
import org.apache.camel.util.component.ApiMethodHelper;
import org.apache.camel.util.component.ApiName;
import org.apache.commons.lang.ClassUtils;
import org.apache.maven.doxia.siterenderer.RenderingContext;
import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
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.apache.maven.reporting.MavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.apache.velocity.VelocityContext;
import org.codehaus.doxia.sink.Sink;
import org.codehaus.plexus.util.StringUtils;
/**
* Generates documentation for API Component.
*/
@Mojo(name = "document", requiresDependencyResolution = ResolutionScope.COMPILE, requiresProject = true,
defaultPhase = LifecyclePhase.SITE)
public class DocumentGeneratorMojo extends AbstractGeneratorMojo implements MavenReport {
// document output directory
@Parameter(property = PREFIX + "reportOutputDirectory",
defaultValue = "${project.reporting.outputDirectory}/cameldocs")
private File reportOutputDirectory;
// name of destination directory
@Parameter(property = PREFIX + "destDir", defaultValue = "cameldocs")
private String destDir;
/**
* The name of the Camel report to be displayed in the Maven Generated Reports page
* (i.e. <code>project-reports.html</code>).
*/
@Parameter(property = "name")
private String name;
/**
* The description of the Camel report to be displayed in the Maven Generated Reports page
* (i.e. <code>project-reports.html</code>).
*/
@Parameter(property = "description")
private String description;
private ApiCollection collection;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
RenderingContext context = new RenderingContext(reportOutputDirectory, getOutputName() + ".html");
SiteRendererSink sink = new SiteRendererSink(context);
Locale locale = Locale.getDefault();
try {
generate(sink, locale);
} catch (MavenReportException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
private void loadApiCollection() throws MavenReportException {
try {
final Class<?> collectionClass = getProjectClassLoader().loadClass(
outPackage + "." + componentName + "ApiCollection");
final Method getCollection = collectionClass.getMethod("getCollection");
this.collection = (ApiCollection) getCollection.invoke(null);
} catch (ClassNotFoundException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (NoSuchMethodException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (InvocationTargetException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (IllegalAccessException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (MojoExecutionException e) {
throw new MavenReportException(e.getMessage(), e);
}
}
private VelocityContext getDocumentContext() throws MavenReportException {
final VelocityContext context = new VelocityContext();
context.put("helper", this);
// project GAV
context.put("groupId", project.getGroupId());
context.put("artifactId", project.getArtifactId());
context.put("version", project.getVersion());
// component URI format
// look for single API, no endpoint-prefix
@SuppressWarnings("unchecked")
final Set<String> apiNames = new TreeSet<String>(collection.getApiNames());
context.put("apiNames", apiNames);
String suffix;
if (apiNames.size() == 1 && ((Set) apiNames).contains("")) {
suffix = "://endpoint?[options]";
} else {
suffix = "://endpoint-prefix/endpoint?[options]";
}
context.put("uriFormat", scheme + suffix);
// API helpers
final Map<String, ApiMethodHelper> apiHelpers = new TreeMap<String, ApiMethodHelper>();
for (Object element : collection.getApiHelpers().entrySet()) {
Map.Entry entry = (Map.Entry) element;
apiHelpers.put(((ApiName) entry.getKey()).getName(), (ApiMethodHelper) entry.getValue());
}
context.put("apiHelpers", apiHelpers);
// API methods and endpoint configurations
final Map<String, Class<? extends ApiMethod>> apiMethods = new TreeMap<String, Class<? extends ApiMethod>>();
final Map<String, Class<?>> apiConfigs = new TreeMap<String, Class<?>>();
for (Object element : collection.getApiMethods().entrySet()) {
Map.Entry entry = (Map.Entry) element;
final String name = ((ApiName) entry.getValue()).getName();
@SuppressWarnings("unchecked")
Class<? extends ApiMethod> apiMethod = (Class<? extends ApiMethod>) entry.getKey();
apiMethods.put(name, apiMethod);
Class<?> configClass;
try {
configClass = getProjectClassLoader().loadClass(getEndpointConfigName(apiMethod));
} catch (ClassNotFoundException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (MojoExecutionException e) {
throw new MavenReportException(e.getMessage(), e);
}
apiConfigs.put(name, configClass);
}
context.put("apiMethods", apiMethods);
context.put("apiConfigs", apiConfigs);
// API component properties
context.put("scheme", this.scheme);
context.put("componentName", this.componentName);
Class<?> configClass;
try {
configClass = getProjectClassLoader().loadClass(getComponentConfig());
} catch (ClassNotFoundException e) {
throw new MavenReportException(e.getMessage(), e);
} catch (MojoExecutionException e) {
throw new MavenReportException(e.getMessage(), e);
}
context.put("componentConfig", configClass);
// get declared and derived fields for component config
// use get/set methods instead of fields, since this class could inherit others, that have private fields
// so getDeclaredFields() won't work, like it does for generated endpoint config classes!!!
final Map<String, String> configFields = new TreeMap<String, String>();
do {
IntrospectionSupport.ClassInfo classInfo = IntrospectionSupport.cacheClass(configClass);
for (IntrospectionSupport.MethodInfo method : classInfo.methods) {
if (method.isSetter) {
configFields.put(method.getterOrSetterShorthandName, getCanonicalName(method.method.getParameterTypes()[0]));
}
}
configClass = configClass.getSuperclass();
} while (configClass != null && !configClass.equals(Object.class));
context.put("componentConfigFields", configFields);
return context;
}
private String getComponentConfig() {
StringBuilder builder = new StringBuilder(componentPackage);
builder.append(".").append(componentName).append("Configuration");
return builder.toString();
}
private String getEndpointConfigName(Class<? extends ApiMethod> apiMethod) {
final String simpleName = apiMethod.getSimpleName();
StringBuilder builder = new StringBuilder(componentPackage);
builder.append(".");
builder.append(simpleName.substring(0, simpleName.indexOf("ApiMethod")));
builder.append("EndpointConfiguration");
return builder.toString();
}
private File getDocumentFile() {
return new File(getReportOutputDirectory(), getDocumentName() + ".html");
}
private String getDocumentName() {
return this.componentName + "Component";
}
@Override
public void generate(Sink sink, Locale locale) throws MavenReportException {
// load APICollection
loadApiCollection();
try {
mergeTemplate(getDocumentContext(), getDocumentFile(), "/api-document.vm");
} catch (MojoExecutionException e) {
throw new MavenReportException(e.getMessage(), e);
}
}
@Override
public String getOutputName() {
return this.destDir + "/" + getDocumentName();
}
@Override
public String getCategoryName() {
return CATEGORY_PROJECT_REPORTS;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getName(Locale locale) {
if (StringUtils.isEmpty(name)) {
return getBundle(locale).getString("report.cameldoc.name");
}
return name;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String getDescription(Locale locale) {
if (StringUtils.isEmpty(description)) {
return getBundle(locale).getString("report.cameldoc.description");
}
return description;
}
@Override
public File getReportOutputDirectory() {
return reportOutputDirectory;
}
@Override
public void setReportOutputDirectory(File reportOutputDirectory) {
updateReportOutputDirectory(reportOutputDirectory);
}
private void updateReportOutputDirectory(File reportOutputDirectory) {
// append destDir if needed
if (this.destDir != null && reportOutputDirectory != null
&& !reportOutputDirectory.getAbsolutePath().endsWith(destDir)) {
this.reportOutputDirectory = new File(reportOutputDirectory, destDir);
} else {
this.reportOutputDirectory = reportOutputDirectory;
}
}
public String getDestDir() {
return destDir;
}
public void setDestDir(String destDir) {
this.destDir = destDir;
updateReportOutputDirectory(this.reportOutputDirectory);
}
@Override
public boolean isExternalReport() {
return true;
}
@Override
public boolean canGenerateReport() {
// TODO check for class availability??
return true;
}
private ResourceBundle getBundle(Locale locale) {
return ResourceBundle.getBundle("cameldoc-report", locale, getClass().getClassLoader());
}
public static List<EndpointInfo> getEndpoints(Class<? extends ApiMethod> apiMethod,
ApiMethodHelper<?> helper, Class<?> endpointConfig) {
// get list of valid options
final Set<String> validOptions = new HashSet<String>();
for (Field field : endpointConfig.getDeclaredFields()) {
validOptions.add(field.getName());
}
// create method name map
final Map<String, List<ApiMethod>> methodMap = new TreeMap<String, List<ApiMethod>>();
for (ApiMethod method : apiMethod.getEnumConstants()) {
String methodName = method.getName();
List<ApiMethod> apiMethods = methodMap.get(methodName);
if (apiMethods == null) {
apiMethods = new ArrayList<ApiMethod>();
methodMap.put(methodName, apiMethods);
}
apiMethods.add(method);
}
// create method name to alias name map
final Map<String, Set<String>> aliasMap = new TreeMap<String, Set<String>>();
final Map<String, Set<String>> aliasToMethodMap = helper.getAliases();
for (Map.Entry<String, Set<String>> entry : aliasToMethodMap.entrySet()) {
final String alias = entry.getKey();
for (String method : entry.getValue()) {
Set<String> aliases = aliasMap.get(method);
if (aliases == null) {
aliases = new TreeSet<String>();
aliasMap.put(method, aliases);
}
aliases.add(alias);
}
}
// create options map and return type map
final Map<String, Set<String>> optionMap = new TreeMap<String, Set<String>>();
final Map<String, Set<String>> returnType = new TreeMap<String, Set<String>>();
for (Map.Entry<String, List<ApiMethod>> entry : methodMap.entrySet()) {
final String name = entry.getKey();
final List<ApiMethod> apiMethods = entry.getValue();
// count the number of times, every valid option shows up across methods
// and also collect return types
final Map<String, Integer> optionCount = new TreeMap<String, Integer>();
final TreeSet<String> resultTypes = new TreeSet<String>();
returnType.put(name, resultTypes);
for (ApiMethod method : apiMethods) {
for (String arg : method.getArgNames()) {
if (validOptions.contains(arg)) {
Integer count = optionCount.get(arg);
if (count == null) {
count = 1;
} else {
count += 1;
}
optionCount.put(arg, count);
}
}
// wrap primitive result types
Class<?> resultType = method.getResultType();
if (resultType.isPrimitive()) {
resultType = ClassUtils.primitiveToWrapper(resultType);
}
resultTypes.add(getCanonicalName(resultType));
}
// collect method options
final TreeSet<String> options = new TreeSet<String>();
optionMap.put(name, options);
final Set<String> mandatory = new TreeSet<String>();
// generate optional and mandatory lists for overloaded methods
int nMethods = apiMethods.size();
for (ApiMethod method : apiMethods) {
final Set<String> optional = new TreeSet<String>();
for (String arg : method.getArgNames()) {
if (validOptions.contains(arg)) {
final Integer count = optionCount.get(arg);
if (count == nMethods) {
mandatory.add(arg);
} else {
optional.add(arg);
}
}
}
if (!optional.isEmpty()) {
options.add(optional.toString());
}
}
if (!mandatory.isEmpty()) {
// strip [] from mandatory options
final String mandatoryOptions = mandatory.toString();
options.add(mandatoryOptions.substring(1, mandatoryOptions.length() - 1));
}
}
// create endpoint data
final List<EndpointInfo> infos = new ArrayList<EndpointInfo>();
for (Map.Entry<String, List<ApiMethod>> methodEntry : methodMap.entrySet()) {
final String endpoint = methodEntry.getKey();
// set endpoint name
EndpointInfo info = new EndpointInfo();
info.endpoint = endpoint;
info.aliases = convertSetToString(aliasMap.get(endpoint));
info.options = convertSetToString(optionMap.get(endpoint));
final Set<String> resultTypes = returnType.get(endpoint);
// get rid of void results
resultTypes.remove("void");
info.resultTypes = convertSetToString(resultTypes);
infos.add(info);
}
return infos;
}
private static String convertSetToString(Set<String> values) {
if (values != null && !values.isEmpty()) {
final String result = values.toString();
return result.substring(1, result.length() - 1);
} else {
return "";
}
}
public static String getCanonicalName(Field field) {
final Type fieldType = field.getGenericType();
if (fieldType instanceof ParameterizedType) {
return getCanonicalName((ParameterizedType) fieldType);
} else {
return getCanonicalName(field.getType());
}
}
private static String getCanonicalName(ParameterizedType fieldType) {
final Type[] typeArguments = fieldType.getActualTypeArguments();
final int nArguments = typeArguments.length;
if (nArguments > 0) {
final StringBuilder result = new StringBuilder(getCanonicalName((Class<?>) fieldType.getRawType()));
result.append("<");
int i = 0;
for (Type typeArg : typeArguments) {
if (typeArg instanceof ParameterizedType) {
result.append(getCanonicalName((ParameterizedType) typeArg));
} else {
result.append(getCanonicalName((Class<?>) typeArg));
}
if (++i < nArguments) {
result.append(',');
}
}
result.append(">");
return result.toString();
}
return getCanonicalName((Class<?>) fieldType.getRawType());
}
public static class EndpointInfo {
private String endpoint;
private String aliases;
private String options;
private String resultTypes;
public String getEndpoint() {
return endpoint;
}
public String getAliases() {
return aliases;
}
public String getOptions() {
return options;
}
public String getResultTypes() {
return resultTypes;
}
}
}