001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.PrintWriter;
025import java.nio.charset.StandardCharsets;
026import java.util.function.Consumer;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import org.apache.commons.cli.CommandLine;
031import org.apache.commons.cli.CommandLineParser;
032import org.apache.commons.cli.DefaultParser;
033import org.apache.commons.cli.HelpFormatter;
034import org.apache.commons.cli.Options;
035import org.apache.commons.cli.ParseException;
036
037import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
038import com.puppycrawl.tools.checkstyle.api.DetailAST;
039import com.puppycrawl.tools.checkstyle.api.DetailNode;
040import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
041import com.puppycrawl.tools.checkstyle.api.TokenTypes;
042import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
043
044/**
045 * This class is used internally in the build process to write a property file
046 * with short descriptions (the first sentences) of TokenTypes constants.
047 * Request: 724871
048 * For IDE plugins (like the eclipse plugin) it would be useful to have
049 * a programmatic access to the first sentence of the TokenType constants,
050 * so they can use them in their configuration gui.
051 */
052public final class JavadocPropertiesGenerator {
053
054    /**
055     * The command line option to specify the output file.
056     */
057    private static final String OPTION_DEST_FILE = "destfile";
058
059    /**
060     * The width of the CLI help option.
061     */
062    private static final int HELP_WIDTH = 100;
063
064    /**
065     * This regexp is used to extract the first sentence from the text.
066     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
067     * "question mark", followed by a space or the end of the text.
068     */
069    private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile("(.*?[.?!])(\\s|$)");
070
071    /**
072     * Don't create instance of this class, use the {@link #main(String[])} method instead.
073     */
074    private JavadocPropertiesGenerator() {
075    }
076
077    /**
078     * TokenTypes.properties generator entry point.
079     * @param args the command line arguments
080     * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
081     * @throws ParseException if the command line can not be passed
082     **/
083    public static void main(String... args)
084            throws CheckstyleException, ParseException {
085        final CommandLine commandLine = parseCli(args);
086        if (commandLine.getArgList().size() == 1) {
087            final File inputFile = new File(commandLine.getArgList().get(0));
088            final File outputFile = new File(commandLine.getOptionValue(OPTION_DEST_FILE));
089            writePropertiesFile(inputFile, outputFile);
090        }
091        else {
092            printUsage();
093        }
094    }
095
096    /**
097     * Creates the .properties file from a .java file.
098     * @param inputFile .java file
099     * @param outputFile .properties file
100     * @throws CheckstyleException if a javadoc comment can not be parsed
101     */
102    private static void writePropertiesFile(File inputFile, File outputFile)
103            throws CheckstyleException {
104        try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8.name())) {
105            final DetailAST top = JavaParser.parseFile(inputFile, JavaParser.Options.WITH_COMMENTS);
106            final DetailAST objBlock = getClassBody(top);
107            if (objBlock != null) {
108                iteratePublicStaticIntFields(objBlock, writer::println);
109            }
110        }
111        catch (IOException ex) {
112            throw new CheckstyleException("Failed to write javadoc properties of '" + inputFile
113                + "' to '" + outputFile + "'", ex);
114        }
115    }
116
117    /**
118     * Walks over the type members and push the first javadoc sentence of every
119     * {@code public} {@code static} {@code int} field to the consumer.
120     * @param objBlock the OBJBLOCK of a class to iterate over its members
121     * @param consumer first javadoc sentence consumer
122     * @throws CheckstyleException if failed to parse a javadoc comment
123     */
124    private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
125            throws CheckstyleException {
126        for (DetailAST member = objBlock.getFirstChild(); member != null;
127                member = member.getNextSibling()) {
128            if (isPublicStaticFinalIntField(member)) {
129                final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
130                final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
131                if (firstJavadocSentence != null) {
132                    consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
133                }
134            }
135        }
136    }
137
138    /**
139     * Finds the class body of the first class in the DetailAST.
140     * @param top AST to find the class body
141     * @return OBJBLOCK token if found; {@code null} otherwise
142     */
143    private static DetailAST getClassBody(DetailAST top) {
144        DetailAST ast = top;
145        while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
146            ast = ast.getNextSibling();
147        }
148        DetailAST objBlock = null;
149        if (ast != null) {
150            objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
151        }
152        return objBlock;
153    }
154
155    /**
156     * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
157     * @param ast to process
158     * @return {@code true} if matches; {@code false} otherwise
159     */
160    private static boolean isPublicStaticFinalIntField(DetailAST ast) {
161        boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
162        if (result) {
163            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
164            result = type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
165            if (result) {
166                final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
167                result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
168                    && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
169                    && modifiers.findFirstToken(TokenTypes.FINAL) != null;
170            }
171        }
172        return result;
173    }
174
175    /**
176     * Extracts the name of an ast.
177     * @param ast to extract the name
178     * @return the text content of the inner {@code TokenTypes.IDENT} node
179     */
180    private static String getName(DetailAST ast) {
181        return ast.findFirstToken(TokenTypes.IDENT).getText();
182    }
183
184    /**
185     * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
186     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
187     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
188     * are converted to HTML code.
189     * @param ast to extract the first sentence
190     * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
191     *      or {@code null} if the first sentence is absent or malformed (does not end with period)
192     * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
193     *      tag found
194     */
195    private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
196        String firstSentence = null;
197        for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null;
198                child = child.getNextSibling()) {
199            // If there is an annotation, the javadoc comment will be a child of it.
200            if (child.getType() == TokenTypes.ANNOTATION) {
201                firstSentence = getFirstJavadocSentence(child);
202            }
203            // Otherwise, the javadoc comment will be right here.
204            else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
205                    && JavadocUtil.isJavadocComment(child)) {
206                final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
207                firstSentence = getFirstJavadocSentence(tree);
208            }
209        }
210        return firstSentence;
211    }
212
213    /**
214     * Extracts the first sentence as HTML formatted text from a DetailNode.
215     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
216     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
217     * are converted to HTML code.
218     * @param tree to extract the first sentence
219     * @return the first sentence of the node or {@code null} if the first sentence is absent or
220     *      malformed (does not end with any of the end-of-sentence markers)
221     * @throws CheckstyleException if an unsupported inline tag found
222     */
223    private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
224        String firstSentence = null;
225        final StringBuilder builder = new StringBuilder(128);
226        for (DetailNode node : tree.getChildren()) {
227            if (node.getType() == JavadocTokenTypes.TEXT) {
228                final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText());
229                if (matcher.find()) {
230                    // Commit the sentence if an end-of-sentence marker is found.
231                    firstSentence = builder.append(matcher.group(1)).toString();
232                    break;
233                }
234                // Otherwise append the whole line and look for an end-of-sentence marker
235                // on the next line.
236                builder.append(node.getText());
237            }
238            else if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
239                formatInlineCodeTag(builder, node);
240            }
241            else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
242                formatHtmlElement(builder, node);
243            }
244        }
245        return firstSentence;
246    }
247
248    /**
249     * Converts inline code tag into HTML form.
250     * @param builder to append
251     * @param inlineTag to format
252     * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag
253     */
254    private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag)
255            throws CheckstyleException {
256        boolean wrapWithCodeTag = false;
257        for (DetailNode node : inlineTag.getChildren()) {
258            switch (node.getType()) {
259                case JavadocTokenTypes.CODE_LITERAL:
260                    wrapWithCodeTag = true;
261                    break;
262                // The text to append.
263                case JavadocTokenTypes.TEXT:
264                    if (wrapWithCodeTag) {
265                        builder.append("<code>").append(node.getText()).append("</code>");
266                    }
267                    else {
268                        builder.append(node.getText());
269                    }
270                    break;
271                // Empty content tags.
272                case JavadocTokenTypes.LITERAL_LITERAL:
273                case JavadocTokenTypes.JAVADOC_INLINE_TAG_START:
274                case JavadocTokenTypes.JAVADOC_INLINE_TAG_END:
275                case JavadocTokenTypes.WS:
276                    break;
277                default:
278                    throw new CheckstyleException("Unsupported inline tag "
279                        + JavadocUtil.getTokenName(node.getType()));
280            }
281        }
282    }
283
284    /**
285     * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
286     * @param builder to append
287     * @param node to format
288     */
289    private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
290        switch (node.getType()) {
291            case JavadocTokenTypes.START:
292            case JavadocTokenTypes.HTML_TAG_NAME:
293            case JavadocTokenTypes.END:
294            case JavadocTokenTypes.TEXT:
295            case JavadocTokenTypes.SLASH:
296                builder.append(node.getText());
297                break;
298            default:
299                for (DetailNode child : node.getChildren()) {
300                    formatHtmlElement(builder, child);
301                }
302                break;
303        }
304    }
305
306    /**
307     *  Prints the usage information.
308     */
309    private static void printUsage() {
310        final HelpFormatter formatter = new HelpFormatter();
311        formatter.setWidth(HELP_WIDTH);
312        formatter.printHelp(String.format("java %s [options] <input file>.",
313            JavadocPropertiesGenerator.class.getName()), buildOptions());
314    }
315
316    /**
317     * Parses command line based on passed arguments.
318     * @param args command line arguments
319     * @return parsed information about passed arguments
320     * @throws ParseException when passed arguments are not valid
321     */
322    private static CommandLine parseCli(String... args)
323            throws ParseException {
324        final CommandLineParser clp = new DefaultParser();
325        return clp.parse(buildOptions(), args);
326    }
327
328    /**
329     * Builds and returns the list of supported parameters.
330     * @return available options
331     */
332    private static Options buildOptions() {
333        final Options options = new Options();
334        options.addRequiredOption(null, OPTION_DEST_FILE, true, "The output file.");
335        return options;
336    }
337
338}