001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.coding;
021
022import java.util.BitSet;
023import java.util.List;
024import java.util.Map;
025import java.util.regex.Pattern;
026
027import com.google.common.collect.Lists;
028import com.google.common.collect.Maps;
029import com.puppycrawl.tools.checkstyle.api.Check;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
034
035/**
036 * Checks for multiple occurrences of the same string literal within a
037 * single file.
038 *
039 * @author Daniel Grenner
040 */
041public class MultipleStringLiteralsCheck extends Check {
042
043    /**
044     * A key is pointing to the warning message text in "messages.properties"
045     * file.
046     */
047    public static final String MSG_KEY = "multiple.string.literal";
048
049    /**
050     * The found strings and their positions.
051     * {@code <String, ArrayList>}, with the ArrayList containing StringInfo
052     * objects.
053     */
054    private final Map<String, List<StringInfo>> stringMap = Maps.newHashMap();
055
056    /**
057     * Marks the TokenTypes where duplicate strings should be ignored.
058     */
059    private final BitSet ignoreOccurrenceContext = new BitSet();
060
061    /**
062     * The allowed number of string duplicates in a file before an error is
063     * generated.
064     */
065    private int allowedDuplicates = 1;
066
067    /**
068     * Pattern for matching ignored strings.
069     */
070    private Pattern pattern;
071
072    /**
073     * Construct an instance with default values.
074     */
075    public MultipleStringLiteralsCheck() {
076        setIgnoreStringsRegexp("^\"\"$");
077        ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
078    }
079
080    /**
081     * Sets the maximum allowed duplicates of a string.
082     * @param allowedDuplicates The maximum number of duplicates.
083     */
084    public void setAllowedDuplicates(int allowedDuplicates) {
085        this.allowedDuplicates = allowedDuplicates;
086    }
087
088    /**
089     * Sets regular expression pattern for ignored strings.
090     * @param ignoreStringsRegexp
091     *        regular expression pattern for ignored strings
092     * @throws org.apache.commons.beanutils.ConversionException
093     *         if unable to create Pattern object
094     */
095    public final void setIgnoreStringsRegexp(String ignoreStringsRegexp) {
096        if (ignoreStringsRegexp != null
097            && !ignoreStringsRegexp.isEmpty()) {
098            pattern = CommonUtils.createPattern(ignoreStringsRegexp);
099        }
100        else {
101            pattern = null;
102        }
103    }
104
105    /**
106     * Adds a set of tokens the check is interested in.
107     * @param strRep the string representation of the tokens interested in
108     */
109    public final void setIgnoreOccurrenceContext(String... strRep) {
110        ignoreOccurrenceContext.clear();
111        for (final String s : strRep) {
112            final int type = TokenUtils.getTokenId(s);
113            ignoreOccurrenceContext.set(type);
114        }
115    }
116
117    @Override
118    public int[] getDefaultTokens() {
119        return getAcceptableTokens();
120    }
121
122    @Override
123    public int[] getAcceptableTokens() {
124        return new int[] {TokenTypes.STRING_LITERAL};
125    }
126
127    @Override
128    public int[] getRequiredTokens() {
129        return getAcceptableTokens();
130    }
131
132    @Override
133    public void visitToken(DetailAST ast) {
134        if (isInIgnoreOccurrenceContext(ast)) {
135            return;
136        }
137        final String currentString = ast.getText();
138        if (pattern == null || !pattern.matcher(currentString).find()) {
139            List<StringInfo> hitList = stringMap.get(currentString);
140            if (hitList == null) {
141                hitList = Lists.newArrayList();
142                stringMap.put(currentString, hitList);
143            }
144            final int line = ast.getLineNo();
145            final int col = ast.getColumnNo();
146            hitList.add(new StringInfo(line, col));
147        }
148    }
149
150    /**
151     * Analyses the path from the AST root to a given AST for occurrences
152     * of the token types in {@link #ignoreOccurrenceContext}.
153     *
154     * @param ast the node from where to start searching towards the root node
155     * @return whether the path from the root node to ast contains one of the
156     *     token type in {@link #ignoreOccurrenceContext}.
157     */
158    private boolean isInIgnoreOccurrenceContext(DetailAST ast) {
159        for (DetailAST token = ast;
160             token.getParent() != null;
161             token = token.getParent()) {
162            final int type = token.getType();
163            if (ignoreOccurrenceContext.get(type)) {
164                return true;
165            }
166        }
167        return false;
168    }
169
170    @Override
171    public void beginTree(DetailAST rootAST) {
172        super.beginTree(rootAST);
173        stringMap.clear();
174    }
175
176    @Override
177    public void finishTree(DetailAST rootAST) {
178        for (Map.Entry<String, List<StringInfo>> stringListEntry : stringMap.entrySet()) {
179            final List<StringInfo> hits = stringListEntry.getValue();
180            if (hits.size() > allowedDuplicates) {
181                final StringInfo firstFinding = hits.get(0);
182                final int line = firstFinding.getLine();
183                final int col = firstFinding.getCol();
184                log(line, col, MSG_KEY, stringListEntry.getKey(), hits.size());
185            }
186        }
187    }
188
189    /**
190     * This class contains information about where a string was found.
191     */
192    private static final class StringInfo {
193        /**
194         * Line of finding.
195         */
196        private final int line;
197        /**
198         * Column of finding.
199         */
200        private final int col;
201        /**
202         * Creates information about a string position.
203         * @param line int
204         * @param col int
205         */
206        StringInfo(int line, int col) {
207            this.line = line;
208            this.col = col;
209        }
210
211        /**
212         * The line where a string was found.
213         * @return int Line of the string.
214         */
215        private int getLine() {
216            return line;
217        }
218
219        /**
220         * The column where a string was found.
221         * @return int Column of the string.
222         */
223        private int getCol() {
224            return col;
225        }
226    }
227
228}