001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.IOException;
024import java.io.InputStream;
025import java.io.Serial;
026import java.nio.file.Files;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Properties;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034import com.puppycrawl.tools.checkstyle.StatelessCheck;
035import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
036import com.puppycrawl.tools.checkstyle.api.FileText;
037
038/**
039 * <div>
040 * Detects duplicated keys in properties files.
041 * </div>
042 *
043 * <p>
044 * Rationale: Multiple property keys usually appear after merge or rebase of
045 * several branches. While there are no problems in runtime, there can be a confusion
046 * due to having different values for the duplicated properties.
047 * </p>
048 * <ul>
049 * <li>
050 * Property {@code fileExtensions} - Specify the file extensions of the files to process.
051 * Type is {@code java.lang.String[]}.
052 * Default value is {@code .properties}.
053 * </li>
054 * </ul>
055 *
056 * <p>
057 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
058 * </p>
059 *
060 * <p>
061 * Violation Message Keys:
062 * </p>
063 * <ul>
064 * <li>
065 * {@code properties.duplicate.property}
066 * </li>
067 * <li>
068 * {@code unable.open.cause}
069 * </li>
070 * </ul>
071 *
072 * @since 5.7
073 */
074@StatelessCheck
075public class UniquePropertiesCheck extends AbstractFileSetCheck {
076
077    /**
078     * Localization key for check violation.
079     */
080    public static final String MSG_KEY = "properties.duplicate.property";
081    /**
082     * Localization key for IO exception occurred on file open.
083     */
084    public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
085
086    /**
087     * Pattern matching single space.
088     */
089    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
090
091    /**
092     * Construct the check with default values.
093     */
094    public UniquePropertiesCheck() {
095        setFileExtensions("properties");
096    }
097
098    /**
099     * Setter to specify the file extensions of the files to process.
100     *
101     * @param extensions the set of file extensions. A missing
102     *         initial '.' character of an extension is automatically added.
103     * @throws IllegalArgumentException is argument is null
104     */
105    @Override
106    public final void setFileExtensions(String... extensions) {
107        super.setFileExtensions(extensions);
108    }
109
110    @Override
111    protected void processFiltered(File file, FileText fileText) {
112        final UniqueProperties properties = new UniqueProperties();
113        try (InputStream inputStream = Files.newInputStream(file.toPath())) {
114            properties.load(inputStream);
115        }
116        catch (IOException exc) {
117            log(1, MSG_IO_EXCEPTION_KEY, file.getPath(),
118                    exc.getLocalizedMessage());
119        }
120
121        for (Entry<String, Integer> duplication : properties
122                .getDuplicatedKeys().entrySet()) {
123            final String keyName = duplication.getKey();
124            final int lineNumber = getLineNumber(fileText, keyName);
125            // Number of occurrences is number of duplications + 1
126            log(lineNumber, MSG_KEY, keyName, duplication.getValue() + 1);
127        }
128    }
129
130    /**
131     * Method returns line number the key is detected in the checked properties
132     * files first.
133     *
134     * @param fileText
135     *            {@link FileText} object contains the lines to process
136     * @param keyName
137     *            key name to look for
138     * @return line number of first occurrence. If no key found in properties
139     *         file, 1 is returned
140     */
141    private static int getLineNumber(FileText fileText, String keyName) {
142        final Pattern keyPattern = getKeyPattern(keyName);
143        int lineNumber = 1;
144        final Matcher matcher = keyPattern.matcher("");
145        for (int index = 0; index < fileText.size(); index++) {
146            final String line = fileText.get(index);
147            matcher.reset(line);
148            if (matcher.matches()) {
149                break;
150            }
151            ++lineNumber;
152        }
153        // -1 as check seeks for the first duplicate occurrence in file,
154        // so it cannot be the last line.
155        if (lineNumber > fileText.size() - 1) {
156            lineNumber = 1;
157        }
158        return lineNumber;
159    }
160
161    /**
162     * Method returns regular expression pattern given key name.
163     *
164     * @param keyName
165     *            key name to look for
166     * @return regular expression pattern given key name
167     */
168    private static Pattern getKeyPattern(String keyName) {
169        final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
170                .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
171        return Pattern.compile(keyPatternString);
172    }
173
174    /**
175     * Properties subclass to store duplicated property keys in a separate map.
176     *
177     * @noinspection ClassExtendsConcreteCollection
178     * @noinspectionreason ClassExtendsConcreteCollection - we require custom
179     *      {@code put} method to find duplicate keys
180     */
181    private static final class UniqueProperties extends Properties {
182
183        /** A unique serial version identifier. */
184        @Serial
185        private static final long serialVersionUID = 1L;
186        /**
187         * Map, holding duplicated keys and their count. Keys are added here only if they
188         * already exist in Properties' inner map.
189         */
190        private final Map<String, Integer> duplicatedKeys = new HashMap<>();
191
192        /**
193         * Puts the value into properties by the key specified.
194         */
195        @Override
196        public synchronized Object put(Object key, Object value) {
197            final Object oldValue = super.put(key, value);
198            if (oldValue != null && key instanceof String keyString) {
199
200                duplicatedKeys.put(keyString,
201                        duplicatedKeys.getOrDefault(keyString, 0) + 1);
202            }
203            return oldValue;
204        }
205
206        /**
207         * Retrieves a collections of duplicated properties keys.
208         *
209         * @return A collection of duplicated keys.
210         */
211        public Map<String, Integer> getDuplicatedKeys() {
212            return new HashMap<>(duplicatedKeys);
213        }
214
215    }
216
217}