001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 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.checks.javadoc;
021
022import java.util.HashMap;
023import java.util.Map;
024
025import org.antlr.v4.runtime.ANTLRInputStream;
026import org.antlr.v4.runtime.BailErrorStrategy;
027import org.antlr.v4.runtime.BaseErrorListener;
028import org.antlr.v4.runtime.CommonTokenStream;
029import org.antlr.v4.runtime.ParserRuleContext;
030import org.antlr.v4.runtime.RecognitionException;
031import org.antlr.v4.runtime.Recognizer;
032import org.antlr.v4.runtime.Token;
033import org.antlr.v4.runtime.misc.ParseCancellationException;
034import org.antlr.v4.runtime.tree.ParseTree;
035import org.antlr.v4.runtime.tree.TerminalNode;
036
037import com.google.common.base.CaseFormat;
038import com.google.common.primitives.Ints;
039import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
040import com.puppycrawl.tools.checkstyle.api.DetailAST;
041import com.puppycrawl.tools.checkstyle.api.DetailNode;
042import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
043import com.puppycrawl.tools.checkstyle.api.TokenTypes;
044import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocLexer;
045import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocParser;
046import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition;
047import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
048
049/**
050 * Base class for Checks that process Javadoc comments.
051 * @author Baratali Izmailov
052 */
053public abstract class AbstractJavadocCheck extends AbstractCheck {
054    /**
055     * Error message key for common javadoc errors.
056     */
057    public static final String MSG_KEY_PARSE_ERROR = "javadoc.parse.error";
058
059    /**
060     * Unrecognized error from antlr parser.
061     */
062    public static final String MSG_KEY_UNRECOGNIZED_ANTLR_ERROR =
063            "javadoc.unrecognized.antlr.error";
064    /**
065     * Message key of error message. Missed close HTML tag breaks structure
066     * of parse tree, so parser stops parsing and generates such error
067     * message. This case is special because parser prints error like
068     * {@code "no viable alternative at input 'b \n *\n'"} and it is not
069     * clear that error is about missed close HTML tag.
070     */
071    public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
072    /**
073     * Message key of error message.
074     */
075    public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
076        "javadoc.wrong.singleton.html.tag";
077
078    /**
079     * Parse error while rule recognition.
080     */
081    public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
082
083    /**
084     * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal}
085     * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
086     */
087    private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE =
088        new ThreadLocal<Map<String, ParseStatus>>() {
089            @Override
090            protected Map<String, ParseStatus> initialValue() {
091                return new HashMap<>();
092            }
093        };
094
095    /**
096     * Custom error listener.
097     */
098    private DescriptiveErrorListener errorListener;
099
100    /**
101     * DetailAST node of considered Javadoc comment that is just a block comment
102     * in Java language syntax tree.
103     */
104    private DetailAST blockCommentAst;
105
106    /**
107     * Returns the default token types a check is interested in.
108     * @return the default token types
109     * @see JavadocTokenTypes
110     */
111    public abstract int[] getDefaultJavadocTokens();
112
113    /**
114     * Called to process a Javadoc token.
115     * @param ast
116     *        the token to process
117     */
118    public abstract void visitJavadocToken(DetailNode ast);
119
120    /**
121     * Called before the starting to process a tree.
122     * @param rootAst
123     *        the root of the tree
124     */
125    public void beginJavadocTree(DetailNode rootAst) {
126        // No code by default, should be overridden only by demand at subclasses
127    }
128
129    /**
130     * Called after finished processing a tree.
131     * @param rootAst
132     *        the root of the tree
133     */
134    public void finishJavadocTree(DetailNode rootAst) {
135        // No code by default, should be overridden only by demand at subclasses
136    }
137
138    /**
139     * Called after all the child nodes have been process.
140     * @param ast
141     *        the token leaving
142     */
143    public void leaveJavadocToken(DetailNode ast) {
144        // No code by default, should be overridden only by demand at subclasses
145    }
146
147    /**
148     * Defined final to not allow JavadocChecks to change default tokens.
149     * @return default tokens
150     */
151    @Override
152    public final int[] getDefaultTokens() {
153        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
154    }
155
156    /**
157     * Defined final because all JavadocChecks require comment nodes.
158     * @return true
159     */
160    @Override
161    public final boolean isCommentNodesRequired() {
162        return true;
163    }
164
165    @Override
166    public final void beginTree(DetailAST rootAST) {
167        TREE_CACHE.get().clear();
168    }
169
170    @Override
171    public final void finishTree(DetailAST rootAST) {
172        TREE_CACHE.get().clear();
173    }
174
175    @Override
176    public final void visitToken(DetailAST blockCommentNode) {
177        if (JavadocUtils.isJavadocComment(blockCommentNode)
178              && isCorrectJavadocPosition(blockCommentNode)) {
179            // store as field, to share with child Checks
180            blockCommentAst = blockCommentNode;
181
182            final String treeCacheKey = blockCommentNode.getLineNo() + ":"
183                    + blockCommentNode.getColumnNo();
184
185            final ParseStatus result;
186
187            if (TREE_CACHE.get().containsKey(treeCacheKey)) {
188                result = TREE_CACHE.get().get(treeCacheKey);
189            }
190            else {
191                result = parseJavadocAsDetailNode(blockCommentNode);
192                TREE_CACHE.get().put(treeCacheKey, result);
193            }
194
195            if (result.getParseErrorMessage() == null) {
196                processTree(result.getTree());
197            }
198            else {
199                final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
200                log(parseErrorMessage.getLineNumber(),
201                        parseErrorMessage.getMessageKey(),
202                        parseErrorMessage.getMessageArguments());
203            }
204        }
205
206    }
207
208    /**
209     * Getter for block comment in Java language syntax tree.
210     * @return A block comment in the syntax tree.
211     */
212    protected DetailAST getBlockCommentAst() {
213        return blockCommentAst;
214    }
215
216    /**
217     * Checks Javadoc comment it's in right place.
218     * From Javadoc util documentation:
219     * "Placement of comments - Documentation comments are recognized only when placed
220     * immediately before class, interface, constructor, method, or field
221     * declarations -- see the class example, method example, and field example.
222     * Documentation comments placed in the body of a method are ignored. Only one
223     * documentation comment per declaration statement is recognized by the Javadoc tool."
224     *
225     * @param blockComment Block comment AST
226     * @return true if Javadoc is in right place
227     */
228    private static boolean isCorrectJavadocPosition(DetailAST blockComment) {
229        return BlockCommentPosition.isOnClass(blockComment)
230                || BlockCommentPosition.isOnInterface(blockComment)
231                || BlockCommentPosition.isOnEnum(blockComment)
232                || BlockCommentPosition.isOnMethod(blockComment)
233                || BlockCommentPosition.isOnField(blockComment)
234                || BlockCommentPosition.isOnConstructor(blockComment)
235                || BlockCommentPosition.isOnEnumConstant(blockComment)
236                || BlockCommentPosition.isOnAnnotationDef(blockComment);
237    }
238
239    /**
240     * Parses Javadoc comment as DetailNode tree.
241     * @param javadocCommentAst
242     *        DetailAST of Javadoc comment
243     * @return DetailNode tree of Javadoc comment
244     */
245    private ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
246        final String javadocComment = JavadocUtils.getJavadocCommentContent(javadocCommentAst);
247
248        // Use a new error listener each time to be able to use
249        // one check instance for multiple files to be checked
250        // without getting side effects.
251        errorListener = new DescriptiveErrorListener();
252
253        // Log messages should have line number in scope of file,
254        // not in scope of Javadoc comment.
255        // Offset is line number of beginning of Javadoc comment.
256        errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
257
258        final ParseStatus result = new ParseStatus();
259
260        try {
261            final ParseTree parseTree = parseJavadocAsParseTree(javadocComment);
262
263            final DetailNode tree = convertParseTreeToDetailNode(parseTree);
264            result.setTree(tree);
265        }
266        catch (ParseCancellationException ex) {
267            // If syntax error occurs then message is printed by error listener
268            // and parser throws this runtime exception to stop parsing.
269            // Just stop processing current Javadoc comment.
270            ParseErrorMessage parseErrorMessage = errorListener.getErrorMessage();
271
272            // There are cases when antlr error listener does not handle syntax error
273            if (parseErrorMessage == null) {
274                parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(),
275                        MSG_KEY_UNRECOGNIZED_ANTLR_ERROR,
276                        javadocCommentAst.getColumnNo(), ex.getMessage());
277            }
278
279            result.setParseErrorMessage(parseErrorMessage);
280        }
281
282        return result;
283    }
284
285    /**
286     * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree.
287     *
288     * @param parseTreeNode root node of ParseTree
289     * @return root of DetailNode tree
290     */
291    private DetailNode convertParseTreeToDetailNode(ParseTree parseTreeNode) {
292        final JavadocNodeImpl rootJavadocNode = createRootJavadocNode(parseTreeNode);
293
294        JavadocNodeImpl currentJavadocParent = rootJavadocNode;
295        ParseTree parseTreeParent = parseTreeNode;
296
297        while (currentJavadocParent != null) {
298            final JavadocNodeImpl[] children =
299                    (JavadocNodeImpl[]) currentJavadocParent.getChildren();
300
301            insertChildrenNodes(children, parseTreeParent);
302
303            if (children.length > 0) {
304                currentJavadocParent = children[0];
305                parseTreeParent = parseTreeParent.getChild(0);
306            }
307            else {
308                JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
309                        .getNextSibling(currentJavadocParent);
310
311                ParseTree nextParseTreeSibling = getNextSibling(parseTreeParent);
312
313                if (nextJavadocSibling == null) {
314                    JavadocNodeImpl tempJavadocParent =
315                            (JavadocNodeImpl) currentJavadocParent.getParent();
316
317                    ParseTree tempParseTreeParent = parseTreeParent.getParent();
318
319                    while (nextJavadocSibling == null && tempJavadocParent != null) {
320
321                        nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
322                                .getNextSibling(tempJavadocParent);
323
324                        nextParseTreeSibling = getNextSibling(tempParseTreeParent);
325
326                        tempJavadocParent = (JavadocNodeImpl) tempJavadocParent.getParent();
327                        tempParseTreeParent = tempParseTreeParent.getParent();
328                    }
329                }
330                currentJavadocParent = nextJavadocSibling;
331                parseTreeParent = nextParseTreeSibling;
332            }
333        }
334
335        return rootJavadocNode;
336    }
337
338    /**
339     * Creates child nodes for each node from 'nodes' array.
340     * @param parseTreeParent original ParseTree parent node
341     * @param nodes array of JavadocNodeImpl nodes
342     */
343    private void insertChildrenNodes(final JavadocNodeImpl[] nodes, ParseTree parseTreeParent) {
344        for (int i = 0; i < nodes.length; i++) {
345            final JavadocNodeImpl currentJavadocNode = nodes[i];
346            final ParseTree currentParseTreeNodeChild = parseTreeParent.getChild(i);
347            final JavadocNodeImpl[] subChildren =
348                    createChildrenNodes(currentJavadocNode, currentParseTreeNodeChild);
349            currentJavadocNode.setChildren(subChildren);
350        }
351    }
352
353    /**
354     * Creates children Javadoc nodes base on ParseTree node's children.
355     * @param parentJavadocNode node that will be parent for created children
356     * @param parseTreeNode original ParseTree node
357     * @return array of Javadoc nodes
358     */
359    private JavadocNodeImpl[]
360            createChildrenNodes(JavadocNodeImpl parentJavadocNode, ParseTree parseTreeNode) {
361        final JavadocNodeImpl[] children =
362                new JavadocNodeImpl[parseTreeNode.getChildCount()];
363
364        for (int j = 0; j < children.length; j++) {
365            final JavadocNodeImpl child =
366                    createJavadocNode(parseTreeNode.getChild(j), parentJavadocNode, j);
367
368            children[j] = child;
369        }
370        return children;
371    }
372
373    /**
374     * Creates root JavadocNodeImpl node base on ParseTree root node.
375     * @param parseTreeNode ParseTree root node
376     * @return root Javadoc node
377     */
378    private JavadocNodeImpl createRootJavadocNode(ParseTree parseTreeNode) {
379        final JavadocNodeImpl rootJavadocNode = createJavadocNode(parseTreeNode, null, -1);
380
381        final int childCount = parseTreeNode.getChildCount();
382        final JavadocNodeImpl[] children = new JavadocNodeImpl[childCount];
383
384        for (int i = 0; i < childCount; i++) {
385            final JavadocNodeImpl child = createJavadocNode(parseTreeNode.getChild(i),
386                    rootJavadocNode, i);
387            children[i] = child;
388        }
389        rootJavadocNode.setChildren(children);
390        return rootJavadocNode;
391    }
392
393    /**
394     * Creates JavadocNodeImpl node on base of ParseTree node.
395     *
396     * @param parseTree ParseTree node
397     * @param parent DetailNode that will be parent of new node
398     * @param index child index that has new node
399     * @return JavadocNodeImpl node on base of ParseTree node.
400     */
401    private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) {
402        final JavadocNodeImpl node = new JavadocNodeImpl();
403        node.setText(parseTree.getText());
404        node.setColumnNumber(getColumn(parseTree));
405        node.setLineNumber(getLine(parseTree) + blockCommentAst.getLineNo());
406        node.setIndex(index);
407        node.setType(getTokenType(parseTree));
408        node.setParent(parent);
409        node.setChildren(new JavadocNodeImpl[parseTree.getChildCount()]);
410        return node;
411    }
412
413    /**
414     * Gets next sibling of ParseTree node.
415     * @param node ParseTree node
416     * @return next sibling of ParseTree node.
417     */
418    private static ParseTree getNextSibling(ParseTree node) {
419        ParseTree nextSibling = null;
420
421        if (node.getParent() != null) {
422            final ParseTree parent = node.getParent();
423            final int childCount = parent.getChildCount();
424
425            int index = 0;
426            while (true) {
427                final ParseTree currentNode = parent.getChild(index);
428                if (currentNode.equals(node)) {
429                    if (index != childCount - 1) {
430                        nextSibling = parent.getChild(index + 1);
431                    }
432                    break;
433                }
434                index++;
435            }
436        }
437        return nextSibling;
438    }
439
440    /**
441     * Gets token type of ParseTree node from JavadocTokenTypes class.
442     * @param node ParseTree node.
443     * @return token type from JavadocTokenTypes
444     */
445    private static int getTokenType(ParseTree node) {
446        final int tokenType;
447
448        if (node.getChildCount() == 0) {
449            tokenType = ((TerminalNode) node).getSymbol().getType();
450        }
451        else {
452            final String className = getNodeClassNameWithoutContext(node);
453            final String typeName =
454                    CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className);
455            tokenType = JavadocUtils.getTokenId(typeName);
456        }
457
458        return tokenType;
459    }
460
461    /**
462     * Gets class name of ParseTree node and removes 'Context' postfix at the
463     * end.
464     * @param node
465     *        ParseTree node.
466     * @return class name without 'Context'
467     */
468    private static String getNodeClassNameWithoutContext(ParseTree node) {
469        final String className = node.getClass().getSimpleName();
470        // remove 'Context' at the end
471        final int contextLength = 7;
472        return className.substring(0, className.length() - contextLength);
473    }
474
475    /**
476     * Gets line number from ParseTree node.
477     * @param tree
478     *        ParseTree node
479     * @return line number
480     */
481    private static int getLine(ParseTree tree) {
482        if (tree instanceof TerminalNode) {
483            return ((TerminalNode) tree).getSymbol().getLine() - 1;
484        }
485        else {
486            final ParserRuleContext rule = (ParserRuleContext) tree;
487            return rule.start.getLine() - 1;
488        }
489    }
490
491    /**
492     * Gets column number from ParseTree node.
493     * @param tree
494     *        ParseTree node
495     * @return column number
496     */
497    private static int getColumn(ParseTree tree) {
498        if (tree instanceof TerminalNode) {
499            return ((TerminalNode) tree).getSymbol().getCharPositionInLine();
500        }
501        else {
502            final ParserRuleContext rule = (ParserRuleContext) tree;
503            return rule.start.getCharPositionInLine();
504        }
505    }
506
507    /**
508     * Parses block comment content as javadoc comment.
509     * @param blockComment
510     *        block comment content.
511     * @return parse tree
512     */
513    private ParseTree parseJavadocAsParseTree(String blockComment) {
514        final ANTLRInputStream input = new ANTLRInputStream(blockComment);
515
516        final JavadocLexer lexer = new JavadocLexer(input);
517
518        // remove default error listeners
519        lexer.removeErrorListeners();
520
521        // add custom error listener that logs parsing errors
522        lexer.addErrorListener(errorListener);
523
524        final CommonTokenStream tokens = new CommonTokenStream(lexer);
525
526        final JavadocParser parser = new JavadocParser(tokens);
527
528        // remove default error listeners
529        parser.removeErrorListeners();
530
531        // add custom error listener that logs syntax errors
532        parser.addErrorListener(errorListener);
533
534        // This strategy stops parsing when parser error occurs.
535        // By default it uses Error Recover Strategy which is slow and useless.
536        parser.setErrorHandler(new BailErrorStrategy());
537
538        return parser.javadoc();
539    }
540
541    /**
542     * Processes JavadocAST tree notifying Check.
543     * @param root
544     *        root of JavadocAST tree.
545     */
546    private void processTree(DetailNode root) {
547        beginJavadocTree(root);
548        walk(root);
549        finishJavadocTree(root);
550    }
551
552    /**
553     * Processes a node calling Check at interested nodes.
554     * @param root
555     *        the root of tree for process
556     */
557    private void walk(DetailNode root) {
558        final int[] defaultTokenTypes = getDefaultJavadocTokens();
559
560        DetailNode curNode = root;
561        while (curNode != null) {
562            final boolean waitsFor = Ints.contains(defaultTokenTypes, curNode.getType());
563
564            if (waitsFor) {
565                visitJavadocToken(curNode);
566            }
567            DetailNode toVisit = JavadocUtils.getFirstChild(curNode);
568            while (curNode != null && toVisit == null) {
569
570                if (waitsFor) {
571                    leaveJavadocToken(curNode);
572                }
573
574                toVisit = JavadocUtils.getNextSibling(curNode);
575                if (toVisit == null) {
576                    curNode = curNode.getParent();
577                }
578            }
579            curNode = toVisit;
580        }
581    }
582
583    /**
584     * Custom error listener for JavadocParser that prints user readable errors.
585     */
586    private static class DescriptiveErrorListener extends BaseErrorListener {
587
588        /**
589         * Offset is line number of beginning of the Javadoc comment. Log
590         * messages should have line number in scope of file, not in scope of
591         * Javadoc comment.
592         */
593        private int offset;
594
595        /**
596         * Error message that appeared while parsing.
597         */
598        private ParseErrorMessage errorMessage;
599
600        /**
601         * Getter for error message during parsing.
602         * @return Error message during parsing.
603         */
604        private ParseErrorMessage getErrorMessage() {
605            return errorMessage;
606        }
607
608        /**
609         * Sets offset. Offset is line number of beginning of the Javadoc
610         * comment. Log messages should have line number in scope of file, not
611         * in scope of Javadoc comment.
612         * @param offset
613         *        offset line number
614         */
615        public void setOffset(int offset) {
616            this.offset = offset;
617        }
618
619        /**
620         * Logs parser errors in Checkstyle manner. Parser can generate error
621         * messages. There is special error that parser can generate. It is
622         * missed close HTML tag. This case is special because parser prints
623         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
624         * is not clear that error is about missed close HTML tag. Other error
625         * messages are not special and logged simply as "Parse Error...".
626         *
627         * <p>{@inheritDoc}
628         */
629        @Override
630        public void syntaxError(
631                Recognizer<?, ?> recognizer, Object offendingSymbol,
632                int line, int charPositionInLine,
633                String msg, RecognitionException ex) {
634            final int lineNumber = offset + line;
635            final Token token = (Token) offendingSymbol;
636
637            if (MSG_JAVADOC_MISSED_HTML_CLOSE.equals(msg)) {
638                errorMessage = new ParseErrorMessage(lineNumber,
639                        MSG_JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText());
640
641                throw new ParseCancellationException(msg);
642            }
643            else if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
644                errorMessage = new ParseErrorMessage(lineNumber,
645                        MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText());
646
647                throw new ParseCancellationException(msg);
648            }
649            else {
650                final int ruleIndex = ex.getCtx().getRuleIndex();
651                final String ruleName = recognizer.getRuleNames()[ruleIndex];
652                final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to(
653                        CaseFormat.UPPER_UNDERSCORE, ruleName);
654
655                errorMessage = new ParseErrorMessage(lineNumber,
656                        MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
657            }
658        }
659    }
660
661    /**
662     * Contains result of parsing javadoc comment: DetailNode tree and parse
663     * error message.
664     */
665    private static class ParseStatus {
666        /**
667         * DetailNode tree (is null if parsing fails).
668         */
669        private DetailNode tree;
670
671        /**
672         * Parse error message (is null if parsing is successful).
673         */
674        private ParseErrorMessage parseErrorMessage;
675
676        /**
677         * Getter for DetailNode tree.
678         * @return DetailNode tree if parsing was successful, null otherwise.
679         */
680        public DetailNode getTree() {
681            return tree;
682        }
683
684        /**
685         * Sets DetailNode tree.
686         * @param tree DetailNode tree.
687         */
688        public void setTree(DetailNode tree) {
689            this.tree = tree;
690        }
691
692        /**
693         * Getter for error message during parsing.
694         * @return Error message if parsing was unsuccessful, null otherwise.
695         */
696        public ParseErrorMessage getParseErrorMessage() {
697            return parseErrorMessage;
698        }
699
700        /**
701         * Sets parse error message.
702         * @param parseErrorMessage Parse error message.
703         */
704        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
705            this.parseErrorMessage = parseErrorMessage;
706        }
707
708    }
709
710    /**
711     * Contains information about parse error message.
712     */
713    private static class ParseErrorMessage {
714        /**
715         * Line number where parse error occurred.
716         */
717        private final int lineNumber;
718
719        /**
720         * Key for error message.
721         */
722        private final String messageKey;
723
724        /**
725         * Error message arguments.
726         */
727        private final Object[] messageArguments;
728
729        /**
730         * Initializes parse error message.
731         *
732         * @param lineNumber line number
733         * @param messageKey message key
734         * @param messageArguments message arguments
735         */
736        ParseErrorMessage(int lineNumber, String messageKey, Object ... messageArguments) {
737            this.lineNumber = lineNumber;
738            this.messageKey = messageKey;
739            this.messageArguments = messageArguments.clone();
740        }
741
742        /**
743         * Getter for line number where parse error occurred.
744         * @return Line number where parse error occurred.
745         */
746        public int getLineNumber() {
747            return lineNumber;
748        }
749
750        /**
751         * Getter for key for error message.
752         * @return Key for error message.
753         */
754        public String getMessageKey() {
755            return messageKey;
756        }
757
758        /**
759         * Getter for error message arguments.
760         * @return Array of error message arguments.
761         */
762        public Object[] getMessageArguments() {
763            return messageArguments.clone();
764        }
765    }
766}