/** * 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; } } }