001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.IOException;
025import java.util.Properties;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import com.google.common.collect.HashMultiset;
030import com.google.common.collect.ImmutableMultiset;
031import com.google.common.collect.Multiset;
032import com.google.common.collect.Multiset.Entry;
033import com.google.common.io.Closeables;
034import com.puppycrawl.tools.checkstyle.StatelessCheck;
035import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
036import com.puppycrawl.tools.checkstyle.api.FileText;
037
038/**
039 * Checks the uniqueness of property keys (left from equal sign) in the
040 * properties file.
041 *
042 * @author Pavel Baranchikov
043 */
044@StatelessCheck
045public class UniquePropertiesCheck extends AbstractFileSetCheck {
046
047    /**
048     * Localization key for check violation.
049     */
050    public static final String MSG_KEY = "properties.duplicate.property";
051    /**
052     * Localization key for IO exception occurred on file open.
053     */
054    public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
055
056    /**
057     * Pattern matching single space.
058     */
059    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
060
061    /**
062     * Construct the check with default values.
063     */
064    public UniquePropertiesCheck() {
065        setFileExtensions("properties");
066    }
067
068    @Override
069    protected void processFiltered(File file, FileText fileText) {
070        final UniqueProperties properties = new UniqueProperties();
071        FileInputStream fileInputStream = null;
072        try {
073            fileInputStream = new FileInputStream(file);
074            properties.load(fileInputStream);
075        }
076        catch (IOException ex) {
077            log(0, MSG_IO_EXCEPTION_KEY, file.getPath(),
078                    ex.getLocalizedMessage());
079        }
080        finally {
081            Closeables.closeQuietly(fileInputStream);
082        }
083
084        for (Entry<String> duplication : properties
085                .getDuplicatedKeys().entrySet()) {
086            final String keyName = duplication.getElement();
087            final int lineNumber = getLineNumber(fileText, keyName);
088            // Number of occurrences is number of duplications + 1
089            log(lineNumber, MSG_KEY, keyName, duplication.getCount() + 1);
090        }
091    }
092
093    /**
094     * Method returns line number the key is detected in the checked properties
095     * files first.
096     *
097     * @param fileText
098     *            {@link FileText} object contains the lines to process
099     * @param keyName
100     *            key name to look for
101     * @return line number of first occurrence. If no key found in properties
102     *         file, 0 is returned
103     */
104    private static int getLineNumber(FileText fileText, String keyName) {
105        final Pattern keyPattern = getKeyPattern(keyName);
106        int lineNumber = 1;
107        final Matcher matcher = keyPattern.matcher("");
108        for (int index = 0; index < fileText.size(); index++) {
109            final String line = fileText.get(index);
110            matcher.reset(line);
111            if (matcher.matches()) {
112                break;
113            }
114            ++lineNumber;
115        }
116        // -1 as check seeks for the first duplicate occurance in file,
117        // so it cannot be the last line.
118        if (lineNumber > fileText.size() - 1) {
119            lineNumber = 0;
120        }
121        return lineNumber;
122    }
123
124    /**
125     * Method returns regular expression pattern given key name.
126     *
127     * @param keyName
128     *            key name to look for
129     * @return regular expression pattern given key name
130     */
131    private static Pattern getKeyPattern(String keyName) {
132        final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
133                .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
134        return Pattern.compile(keyPatternString);
135    }
136
137    /**
138     * Properties subclass to store duplicated property keys in a separate map.
139     *
140     * @author Pavel Baranchikov
141     * @noinspection ClassExtendsConcreteCollection, SerializableHasSerializationMethods
142     */
143    private static class UniqueProperties extends Properties {
144        private static final long serialVersionUID = 1L;
145        /**
146         * Multiset, holding duplicated keys. Keys are added here only if they
147         * already exist in Properties' inner map.
148         */
149        private final Multiset<String> duplicatedKeys = HashMultiset
150                .create();
151
152        /**
153         * Puts the value into properties by the key specified.
154         * @noinspection UseOfPropertiesAsHashtable
155         */
156        @Override
157        public synchronized Object put(Object key, Object value) {
158            final Object oldValue = super.put(key, value);
159            if (oldValue != null && key instanceof String) {
160                final String keyString = (String) key;
161                duplicatedKeys.add(keyString);
162            }
163            return oldValue;
164        }
165
166        /**
167         * Retrieves a collections of duplicated properties keys.
168         *
169         * @return A collection of duplicated keys.
170         */
171        public Multiset<String> getDuplicatedKeys() {
172            return ImmutableMultiset.copyOf(duplicatedKeys);
173        }
174    }
175}