org.apache.camel.maven.JavadocApiMethodGeneratorMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.camel.maven.JavadocApiMethodGeneratorMojo.java

Source

/**
 * 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.ChangedCharSetException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.parser.DTD;
import javax.swing.text.html.parser.Parser;
import javax.swing.text.html.parser.TagElement;

import org.apache.camel.util.component.ApiMethodParser;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.maven.plugin.MojoExecutionException;
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.codehaus.plexus.util.IOUtil;

/**
 * Parses ApiMethod signatures from Javadoc.
 */
@Mojo(name = "fromJavadoc", requiresDependencyResolution = ResolutionScope.TEST, requiresProject = true, defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class JavadocApiMethodGeneratorMojo extends AbstractApiMethodGeneratorMojo {

    static {
        // set Java AWT to headless before using Swing HTML parser
        System.setProperty("java.awt.headless", "true");
    }

    protected static final String DEFAULT_EXCLUDE_PACKAGES = "javax?\\.lang.*";
    private static final Pattern ARGTYPES_PATTERN = Pattern.compile("\\s*([^<\\s,]+\\s*(<[^>]+>)?)\\s*,?");
    private static final Pattern RAW_ARGTYPES_PATTERN = Pattern.compile("\\s*([^<\\s,]+)\\s*(<[^>]+>)?\\s*,?");

    @Parameter(property = PREFIX + "excludePackages", defaultValue = DEFAULT_EXCLUDE_PACKAGES)
    protected String excludePackages;

    @Parameter(property = PREFIX + "excludeClasses")
    protected String excludeClasses;

    @Parameter(property = PREFIX + "includeMethods")
    protected String includeMethods;

    @Parameter(property = PREFIX + "excludeMethods")
    protected String excludeMethods;

    @Parameter(property = PREFIX + "includeStaticMethods")
    protected Boolean includeStaticMethods;

    @Override
    public List<String> getSignatureList() throws MojoExecutionException {
        // signatures as map from signature with no arg names to arg names from JavadocParser
        Map<String, String> result = new HashMap<String, String>();

        final Pattern packagePatterns = Pattern.compile(excludePackages);
        final Pattern classPatterns = (excludeClasses != null) ? Pattern.compile(excludeClasses) : null;
        final Pattern includeMethodPatterns = (includeMethods != null) ? Pattern.compile(includeMethods) : null;
        final Pattern excludeMethodPatterns = (excludeMethods != null) ? Pattern.compile(excludeMethods) : null;

        // for proxy class and super classes not matching excluded packages or classes
        for (Class aClass = getProxyType(); aClass != null
                && !packagePatterns.matcher(aClass.getPackage().getName()).matches()
                && (classPatterns == null
                        || !classPatterns.matcher(aClass.getSimpleName()).matches()); aClass = aClass
                                .getSuperclass()) {

            log.debug("Processing " + aClass.getName());
            final String javaDocPath = aClass.getName().replaceAll("\\.", "/") + ".html";

            // read javadoc html text for class
            InputStream inputStream = null;
            try {
                inputStream = getProjectClassLoader().getResourceAsStream(javaDocPath);
                if (inputStream == null) {
                    log.debug("JavaDoc not found on classpath for " + aClass.getName());
                    break;
                }
                // transform the HTML to get method summary as text
                // dummy DTD
                final DTD dtd = DTD.getDTD("html.dtd");
                final JavadocParser htmlParser = new JavadocParser(dtd, javaDocPath);
                htmlParser.parse(new InputStreamReader(inputStream, "UTF-8"));

                // look for parse errors
                final String parseError = htmlParser.getErrorMessage();
                if (parseError != null) {
                    throw new MojoExecutionException(parseError);
                }

                // get public method signature
                final Map<String, String> methodMap = htmlParser.getMethodText();
                for (String method : htmlParser.getMethods()) {
                    if (!result.containsKey(method)
                            && (includeMethodPatterns == null || includeMethodPatterns.matcher(method).find())
                            && (excludeMethodPatterns == null || !excludeMethodPatterns.matcher(method).find())) {

                        final int leftBracket = method.indexOf('(');
                        final String name = method.substring(0, leftBracket);
                        final String args = method.substring(leftBracket + 1, method.length() - 1);
                        String[] types;
                        if (args.isEmpty()) {
                            types = new String[0];
                        } else {
                            // get raw types from args
                            final List<String> rawTypes = new ArrayList<String>();
                            final Matcher argTypesMatcher = RAW_ARGTYPES_PATTERN.matcher(args);
                            while (argTypesMatcher.find()) {
                                rawTypes.add(argTypesMatcher.group(1));
                            }
                            types = rawTypes.toArray(new String[rawTypes.size()]);
                        }
                        final String resultType = getResultType(aClass, name, types);
                        if (resultType != null) {
                            final StringBuilder signature = new StringBuilder(resultType);
                            signature.append(" ").append(name).append(methodMap.get(method));
                            result.put(method, signature.toString());
                        }
                    }
                }
            } catch (IOException e) {
                throw new MojoExecutionException(e.getMessage(), e);
            } finally {
                IOUtil.close(inputStream);
            }
        }

        if (result.isEmpty()) {
            throw new MojoExecutionException("No public non-static methods found, "
                    + "make sure Javadoc is available as project test dependency");
        }
        return new ArrayList<String>(result.values());
    }

    private String getResultType(Class<?> aClass, String name, String[] types) throws MojoExecutionException {
        Class<?>[] argTypes = new Class<?>[types.length];
        final ClassLoader classLoader = getProjectClassLoader();
        for (int i = 0; i < types.length; i++) {
            try {
                try {
                    argTypes[i] = ApiMethodParser.forName(types[i], classLoader);
                } catch (ClassNotFoundException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                }
            } catch (IllegalArgumentException e) {
                throw new MojoExecutionException(e.getCause().getMessage(), e.getCause());
            }
        }

        // return null for non-public methods, and for non-static methods if includeStaticMethods is null or false
        String result = null;
        try {
            final Method method = aClass.getMethod(name, argTypes);
            int modifiers = method.getModifiers();
            if (!Modifier.isStatic(modifiers) || Boolean.TRUE.equals(includeStaticMethods)) {
                result = method.getReturnType().getCanonicalName();
            }
        } catch (NoSuchMethodException e) {
            // could be a non-public method
            try {
                aClass.getDeclaredMethod(name, argTypes);
            } catch (NoSuchMethodException e1) {
                throw new MojoExecutionException(e1.getMessage(), e1);
            }
        }

        return result;
    }

    private static class JavadocParser extends Parser {
        private static final String NON_BREAKING_SPACE = "\u00A0";
        private String hrefPattern;

        private ParserState parserState;
        private String methodWithTypes;
        private StringBuilder methodTextBuilder = new StringBuilder();

        private List<String> methods = new ArrayList<String>();
        private Map<String, String> methodText = new HashMap<String, String>();
        private String errorMessage;

        public JavadocParser(DTD dtd, String docPath) {
            super(dtd);
            this.hrefPattern = docPath + "#";
        }

        @Override
        protected void startTag(TagElement tag) throws ChangedCharSetException {
            super.startTag(tag);

            final HTML.Tag htmlTag = tag.getHTMLTag();
            if (htmlTag != null) {
                if (HTML.Tag.A.equals(htmlTag)) {
                    final SimpleAttributeSet attributes = getAttributes();
                    final Object name = attributes.getAttribute(HTML.Attribute.NAME);
                    if (name != null) {
                        final String nameAttr = (String) name;
                        if (parserState == null && "method_summary".equals(nameAttr)) {
                            parserState = ParserState.METHOD_SUMMARY;
                        } else if (parserState == ParserState.METHOD_SUMMARY
                                && nameAttr.startsWith("methods_inherited_from_class_")) {
                            parserState = null;
                        } else if (parserState == ParserState.METHOD && methodWithTypes == null) {
                            final Object href = attributes.getAttribute(HTML.Attribute.HREF);
                            if (href != null) {
                                String hrefAttr = (String) href;
                                if (hrefAttr.contains(hrefPattern)) {
                                    // unescape HTML
                                    methodWithTypes = unescapeHtml(hrefAttr.substring(hrefAttr.indexOf('#') + 1));
                                }
                            }
                        }
                    }
                } else if (parserState == ParserState.METHOD_SUMMARY && HTML.Tag.CODE.equals(htmlTag)) {
                    parserState = ParserState.METHOD;
                }
            }
        }

        private static String unescapeHtml(String htmlString) {
            return StringEscapeUtils.unescapeHtml(htmlString).replaceAll(NON_BREAKING_SPACE, " ");
        }

        @Override
        protected void handleEmptyTag(TagElement tag) {
            if (parserState == ParserState.METHOD && HTML.Tag.CODE.equals(tag.getHTMLTag())) {
                if (methodWithTypes != null) {
                    // process collected method data
                    methods.add(methodWithTypes);
                    this.methodText.put(methodWithTypes, getArgSignature());

                    // clear the text builder for next method
                    methodTextBuilder.delete(0, methodTextBuilder.length());
                    methodWithTypes = null;
                }

                parserState = ParserState.METHOD_SUMMARY;
            }
        }

        private String getArgSignature() {
            final String typeString = methodWithTypes.substring(methodWithTypes.indexOf('(') + 1,
                    methodWithTypes.indexOf(')'));
            if (typeString.isEmpty()) {
                return "()";
            }

            // split types list
            final List<String> typeList = new ArrayList<String>();
            final Matcher typeMatcher = ARGTYPES_PATTERN.matcher(typeString);
            while (typeMatcher.find()) {
                typeList.add(typeMatcher.group(1).replaceAll(" ", ""));
            }

            // unescape HTML method text
            final String plainText = unescapeHtml(methodTextBuilder.toString());
            final String argsString = plainText.substring(plainText.indexOf('(') + 1, plainText.indexOf(')'));
            final Matcher argMatcher = ApiMethodParser.ARGS_PATTERN.matcher(argsString);
            final List<String> argNames = new ArrayList<String>();
            while (argMatcher.find()) {
                argNames.add(argMatcher.group(3));
            }

            // make sure number of types and names match
            if (typeList.size() != argNames.size()) {
                throw new IllegalArgumentException(
                        "Unexpected Javadoc error, different number of arg types and names");
            }

            final String[] names = argNames.toArray(new String[argNames.size()]);
            StringBuilder builder = new StringBuilder("(");
            int i = 0;
            for (String type : typeList) {
                // split on space or non-breaking space
                builder.append(type).append(" ").append(names[i++]).append(",");
            }
            builder.deleteCharAt(builder.length() - 1);
            builder.append(")");
            return builder.toString();
        }

        @Override
        protected void handleText(char[] text) {
            if (parserState == ParserState.METHOD && methodWithTypes != null) {
                methodTextBuilder.append(text);
            }
        }

        @Override
        protected void handleError(int ln, String msg) {
            if (msg.startsWith("exception ")) {
                this.errorMessage = "Exception parsing Javadoc line " + ln + ": " + msg;
            }
        }

        private String getErrorMessage() {
            return errorMessage;
        }

        private List<String> getMethods() {
            return methods;
        }

        private Map<String, String> getMethodText() {
            return methodText;
        }
    }

    private static enum ParserState {
        METHOD_SUMMARY, METHOD;
    }
}