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