001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 occurrence 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
145        private static final long serialVersionUID = 1L;
146        /**
147         * Multiset, holding duplicated keys. Keys are added here only if they
148         * already exist in Properties' inner map.
149         */
150        private final Multiset<String> duplicatedKeys = HashMultiset
151                .create();
152
153        /**
154         * Puts the value into properties by the key specified.
155         * @noinspection UseOfPropertiesAsHashtable
156         */
157        @Override
158        public synchronized Object put(Object key, Object value) {
159            final Object oldValue = super.put(key, value);
160            if (oldValue != null && key instanceof String) {
161                final String keyString = (String) key;
162                duplicatedKeys.add(keyString);
163            }
164            return oldValue;
165        }
166
167        /**
168         * Retrieves a collections of duplicated properties keys.
169         *
170         * @return A collection of duplicated keys.
171         */
172        public Multiset<String> getDuplicatedKeys() {
173            return ImmutableMultiset.copyOf(duplicatedKeys);
174        }
175
176    }
177
178}