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>> mStringMap = Maps.newHashMap();
047
048    /**
049     * Marks the TokenTypes where duplicate strings should be ignored.
050     */
051    private final BitSet mIgnoreOccurrenceContext = new BitSet();
052
053    /**
054     * The allowed number of string duplicates in a file before an error is
055     * generated.
056     */
057    private int mAllowedDuplicates = 1;
058
059    /**
060     * Sets the maximum allowed duplicates of a string.
061     * @param aAllowedDuplicates The maximum number of duplicates.
062     */
063    public void setAllowedDuplicates(int aAllowedDuplicates)
064    {
065        mAllowedDuplicates = aAllowedDuplicates;
066    }
067
068    /**
069     * Pattern for matching ignored strings.
070     */
071    private Pattern mPattern;
072
073    /**
074     * Construct an instance with default values.
075     */
076    public MultipleStringLiteralsCheck()
077    {
078        setIgnoreStringsRegexp("^\"\"$");
079        mIgnoreOccurrenceContext.set(TokenTypes.ANNOTATION);
080    }
081
082    /**
083     * Sets regexp pattern for ignored strings.
084     * @param aIgnoreStringsRegexp regexp pattern for ignored strings
085     */
086    public void setIgnoreStringsRegexp(String aIgnoreStringsRegexp)
087    {
088        if ((aIgnoreStringsRegexp != null)
089            && (aIgnoreStringsRegexp.length() > 0))
090        {
091            mPattern = Utils.getPattern(aIgnoreStringsRegexp);
092        }
093        else {
094            mPattern = null;
095        }
096    }
097
098    /**
099     * Adds a set of tokens the check is interested in.
100     * @param aStrRep the string representation of the tokens interested in
101     */
102    public final void setIgnoreOccurrenceContext(String[] aStrRep)
103    {
104        mIgnoreOccurrenceContext.clear();
105        for (final String s : aStrRep) {
106            final int type = TokenTypes.getTokenId(s);
107            mIgnoreOccurrenceContext.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 aAST)
119    {
120        if (isInIgnoreOccurrenceContext(aAST)) {
121            return;
122        }
123        final String currentString = aAST.getText();
124        if ((mPattern == null) || !mPattern.matcher(currentString).find()) {
125            List<StringInfo> hitList = mStringMap.get(currentString);
126            if (hitList == null) {
127                hitList = Lists.newArrayList();
128                mStringMap.put(currentString, hitList);
129            }
130            final int line = aAST.getLineNo();
131            final int col = aAST.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 #mIgnoreOccurrenceContext}.
139     *
140     * @param aAST the node from where to start searching towards the root node
141     * @return whether the path from the root node to aAST contains one of the
142     * token type in {@link #mIgnoreOccurrenceContext}.
143     */
144    private boolean isInIgnoreOccurrenceContext(DetailAST aAST)
145    {
146        for (DetailAST token = aAST;
147             token.getParent() != null;
148             token = token.getParent())
149        {
150            final int type = token.getType();
151            if (mIgnoreOccurrenceContext.get(type)) {
152                return true;
153            }
154        }
155        return false;
156    }
157
158    @Override
159    public void beginTree(DetailAST aRootAST)
160    {
161        super.beginTree(aRootAST);
162        mStringMap.clear();
163    }
164
165    @Override
166    public void finishTree(DetailAST aRootAST)
167    {
168        final Set<String> keys = mStringMap.keySet();
169        for (String key : keys) {
170            final List<StringInfo> hits = mStringMap.get(key);
171            if (hits.size() > mAllowedDuplicates) {
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 mLine;
189        /**
190         * Column of finding
191         */
192        private final int mCol;
193        /**
194         * Creates information about a string position.
195         * @param aLine int
196         * @param aCol int
197         */
198        private StringInfo(int aLine, int aCol)
199        {
200            mLine = aLine;
201            mCol = aCol;
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 mLine;
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 mCol;
220        }
221    }
222
223}