com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator.java

Source

////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2018 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;

/**
 * This class is used internally in the build process to write a property file
 * with short descriptions (the first sentences) of TokenTypes constants.
 * Request: 724871
 * For IDE plugins (like the eclipse plugin) it would be useful to have
 * a programmatic access to the first sentence of the TokenType constants,
 * so they can use them in their configuration gui.
 * @author Pavel Bludov
 */
public final class JavadocPropertiesGenerator {

    /**
     * The command line option to specify the output file.
     */
    private static final String OPTION_DEST_FILE = "destfile";

    /**
     * The width of the CLI help option.
     */
    private static final int HELP_WIDTH = 100;

    /**
     * This regexp is used to extract the first sentence from the text.
     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
     * "question mark", followed by a space or the end of the text.
     */
    private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile("(.*?[.?!])(\\s|$)");

    /**
     * Don't create instance of this class, use the {@link #main(String[])} method instead.
     */
    private JavadocPropertiesGenerator() {
    }

    /**
     * TokenTypes.properties generator entry point.
     * @param args the command line arguments
     * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
     * @throws ParseException if the command line can not be passed
     **/
    public static void main(String... args) throws CheckstyleException, ParseException {
        final CommandLine commandLine = parseCli(args);
        if (commandLine.getArgList().size() == 1) {
            final File inputFile = new File(commandLine.getArgList().get(0));
            final File outputFile = new File(commandLine.getOptionValue(OPTION_DEST_FILE));
            writePropertiesFile(inputFile, outputFile);
        } else {
            printUsage();
        }
    }

    /**
     * Creates the .properties file from a .java file.
     * @param inputFile .java file
     * @param outputFile .properties file
     * @throws CheckstyleException if a javadoc comment can not be parsed
     */
    private static void writePropertiesFile(File inputFile, File outputFile) throws CheckstyleException {
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(outputFile, StandardCharsets.UTF_8.name());

            final DetailAST top = JavaParser.parseFile(inputFile, JavaParser.Options.WITH_COMMENTS);
            final DetailAST objBlock = getClassBody(top);
            if (objBlock != null) {
                iteratePublicStaticIntFields(objBlock, writer::println);
            }
        } catch (IOException ex) {
            throw new CheckstyleException(
                    "Failed to write javadoc properties of '" + inputFile + "' to '" + outputFile + "'", ex);
        } finally {
            CommonUtils.close(writer);
        }
    }

    /**
     * Walks over the type members and push the first javadoc sentence of every
     * {@code public} {@code static} {@code int} field to the consumer.
     * @param objBlock the OBJBLOCK of a class to iterate over its members
     * @param consumer first javadoc sentence consumer
     * @throws CheckstyleException if failed to parse a javadoc comment
     */
    private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
            throws CheckstyleException {
        for (DetailAST member = objBlock.getFirstChild(); member != null; member = member.getNextSibling()) {
            if (isPublicStaticFinalIntField(member)) {
                final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
                final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
                if (firstJavadocSentence != null) {
                    consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
                }
            }
        }
    }

    /**
     * Finds the class body of the first class in the DetailAST.
     * @param top AST to find the class body
     * @return OBJBLOCK token if found; {@code null} otherwise
     */
    private static DetailAST getClassBody(DetailAST top) {
        DetailAST ast = top;
        while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
            ast = ast.getNextSibling();
        }
        DetailAST objBlock = null;
        if (ast != null) {
            objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
        }
        return objBlock;
    }

    /**
     * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
     * @param ast to process
     * @return {@code true} if matches; {@code false} otherwise
     */
    private static boolean isPublicStaticFinalIntField(DetailAST ast) {
        boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
        if (result) {
            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
            result = type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
            if (result) {
                final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
                result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
                        && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
                        && modifiers.findFirstToken(TokenTypes.FINAL) != null;
            }
        }
        return result;
    }

    /**
     * Extracts the name of an ast.
     * @param ast to extract the name
     * @return the text content of the inner {@code TokenTypes.IDENT} node
     */
    private static String getName(DetailAST ast) {
        return ast.findFirstToken(TokenTypes.IDENT).getText();
    }

    /**
     * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
     * are converted to HTML code.
     * @param ast to extract the first sentence
     * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
     *      or {@code null} if the first sentence is absent or malformed (does not end with period)
     * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
     *      tag found
     */
    private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
        String firstSentence = null;
        for (DetailAST child = ast.getFirstChild(); child != null
                && firstSentence == null; child = child.getNextSibling()) {
            // If there is an annotation, the javadoc comment will be a child of it.
            if (child.getType() == TokenTypes.ANNOTATION) {
                firstSentence = getFirstJavadocSentence(child);
            }
            // Otherwise, the javadoc comment will be right here.
            else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && JavadocUtils.isJavadocComment(child)) {
                final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
                firstSentence = getFirstJavadocSentence(tree);
            }
        }
        return firstSentence;
    }

    /**
     * Extracts the first sentence as HTML formatted text from a DetailNode.
     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
     * are converted to HTML code.
     * @param tree to extract the first sentence
     * @return the first sentence of the node or {@code null} if the first sentence is absent or
     *      malformed (does not end with any of the end-of-sentence markers)
     * @throws CheckstyleException if an unsupported inline tag found
     */
    private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
        String firstSentence = null;
        final StringBuilder builder = new StringBuilder(128);
        for (DetailNode node : tree.getChildren()) {
            if (node.getType() == JavadocTokenTypes.TEXT) {
                final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText());
                if (matcher.find()) {
                    // Commit the sentence if an end-of-sentence marker is found.
                    firstSentence = builder.append(matcher.group(1)).toString();
                    break;
                }
                // Otherwise append the whole line and look for an end-of-sentence marker
                // on the next line.
                builder.append(node.getText());
            } else if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
                formatInlineCodeTag(builder, node);
            } else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
                formatHtmlElement(builder, node);
            }
        }
        return firstSentence;
    }

    /**
     * Converts inline code tag into HTML form.
     * @param builder to append
     * @param inlineTag to format
     * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag
     */
    private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag)
            throws CheckstyleException {
        boolean wrapWithCodeTag = false;
        for (DetailNode node : inlineTag.getChildren()) {
            switch (node.getType()) {
            case JavadocTokenTypes.CODE_LITERAL:
                wrapWithCodeTag = true;
                break;
            // The text to append.
            case JavadocTokenTypes.TEXT:
                if (wrapWithCodeTag) {
                    builder.append("<code>").append(node.getText()).append("</code>");
                } else {
                    builder.append(node.getText());
                }
                break;
            // Empty content tags.
            case JavadocTokenTypes.LITERAL_LITERAL:
            case JavadocTokenTypes.JAVADOC_INLINE_TAG_START:
            case JavadocTokenTypes.JAVADOC_INLINE_TAG_END:
            case JavadocTokenTypes.WS:
                break;
            default:
                throw new CheckstyleException(
                        "Unsupported inline tag " + JavadocUtils.getTokenName(node.getType()));
            }
        }
    }

    /**
     * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
     * @param builder to append
     * @param node to format
     */
    private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
        switch (node.getType()) {
        case JavadocTokenTypes.START:
        case JavadocTokenTypes.HTML_TAG_NAME:
        case JavadocTokenTypes.END:
        case JavadocTokenTypes.TEXT:
        case JavadocTokenTypes.SLASH:
            builder.append(node.getText());
            break;
        default:
            for (DetailNode child : node.getChildren()) {
                formatHtmlElement(builder, child);
            }
            break;
        }
    }

    /**
     *  Prints the usage information.
     */
    private static void printUsage() {
        final HelpFormatter formatter = new HelpFormatter();
        formatter.setWidth(HELP_WIDTH);
        formatter.printHelp(
                String.format("java %s [options] <input file>.", JavadocPropertiesGenerator.class.getName()),
                buildOptions());
    }

    /**
     * Parses command line based on passed arguments.
     * @param args command line arguments
     * @return parsed information about passed arguments
     * @throws ParseException when passed arguments are not valid
     */
    private static CommandLine parseCli(String... args) throws ParseException {
        final CommandLineParser clp = new DefaultParser();
        return clp.parse(buildOptions(), args);
    }

    /**
     * Builds and returns the list of supported parameters.
     * @return available options
     */
    private static Options buildOptions() {
        final Options options = new Options();
        options.addRequiredOption(null, OPTION_DEST_FILE, true, "The output file.");
        return options;
    }

}