View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.nio.file.Files;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.Properties;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  
33  import com.puppycrawl.tools.checkstyle.StatelessCheck;
34  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
35  import com.puppycrawl.tools.checkstyle.api.FileText;
36  
37  /**
38   * <div>
39   * Detects duplicated keys in properties files.
40   * </div>
41   *
42   * <p>
43   * Rationale: Multiple property keys usually appear after merge or rebase of
44   * several branches. While there are no problems in runtime, there can be a confusion
45   * due to having different values for the duplicated properties.
46   * </p>
47   * <ul>
48   * <li>
49   * Property {@code fileExtensions} - Specify the file extensions of the files to process.
50   * Type is {@code java.lang.String[]}.
51   * Default value is {@code .properties}.
52   * </li>
53   * </ul>
54   *
55   * <p>
56   * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
57   * </p>
58   *
59   * <p>
60   * Violation Message Keys:
61   * </p>
62   * <ul>
63   * <li>
64   * {@code properties.duplicate.property}
65   * </li>
66   * <li>
67   * {@code unable.open.cause}
68   * </li>
69   * </ul>
70   *
71   * @since 5.7
72   */
73  @StatelessCheck
74  public class UniquePropertiesCheck extends AbstractFileSetCheck {
75  
76      /**
77       * Localization key for check violation.
78       */
79      public static final String MSG_KEY = "properties.duplicate.property";
80      /**
81       * Localization key for IO exception occurred on file open.
82       */
83      public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
84  
85      /**
86       * Pattern matching single space.
87       */
88      private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
89  
90      /**
91       * Construct the check with default values.
92       */
93      public UniquePropertiesCheck() {
94          setFileExtensions("properties");
95      }
96  
97      @Override
98      protected void processFiltered(File file, FileText fileText) {
99          final UniqueProperties properties = new UniqueProperties();
100         try (InputStream inputStream = Files.newInputStream(file.toPath())) {
101             properties.load(inputStream);
102         }
103         catch (IOException ex) {
104             log(1, MSG_IO_EXCEPTION_KEY, file.getPath(),
105                     ex.getLocalizedMessage());
106         }
107 
108         for (Entry<String, Integer> duplication : properties
109                 .getDuplicatedKeys().entrySet()) {
110             final String keyName = duplication.getKey();
111             final int lineNumber = getLineNumber(fileText, keyName);
112             // Number of occurrences is number of duplications + 1
113             log(lineNumber, MSG_KEY, keyName, duplication.getValue() + 1);
114         }
115     }
116 
117     /**
118      * Method returns line number the key is detected in the checked properties
119      * files first.
120      *
121      * @param fileText
122      *            {@link FileText} object contains the lines to process
123      * @param keyName
124      *            key name to look for
125      * @return line number of first occurrence. If no key found in properties
126      *         file, 1 is returned
127      */
128     private static int getLineNumber(FileText fileText, String keyName) {
129         final Pattern keyPattern = getKeyPattern(keyName);
130         int lineNumber = 1;
131         final Matcher matcher = keyPattern.matcher("");
132         for (int index = 0; index < fileText.size(); index++) {
133             final String line = fileText.get(index);
134             matcher.reset(line);
135             if (matcher.matches()) {
136                 break;
137             }
138             ++lineNumber;
139         }
140         // -1 as check seeks for the first duplicate occurrence in file,
141         // so it cannot be the last line.
142         if (lineNumber > fileText.size() - 1) {
143             lineNumber = 1;
144         }
145         return lineNumber;
146     }
147 
148     /**
149      * Method returns regular expression pattern given key name.
150      *
151      * @param keyName
152      *            key name to look for
153      * @return regular expression pattern given key name
154      */
155     private static Pattern getKeyPattern(String keyName) {
156         final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
157                 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
158         return Pattern.compile(keyPatternString);
159     }
160 
161     /**
162      * Properties subclass to store duplicated property keys in a separate map.
163      *
164      * @noinspection ClassExtendsConcreteCollection
165      * @noinspectionreason ClassExtendsConcreteCollection - we require custom
166      *      {@code put} method to find duplicate keys
167      */
168     private static final class UniqueProperties extends Properties {
169 
170         /** A unique serial version identifier. */
171         private static final long serialVersionUID = 1L;
172         /**
173          * Map, holding duplicated keys and their count. Keys are added here only if they
174          * already exist in Properties' inner map.
175          */
176         private final Map<String, Integer> duplicatedKeys = new HashMap<>();
177 
178         /**
179          * Puts the value into properties by the key specified.
180          */
181         @Override
182         public synchronized Object put(Object key, Object value) {
183             final Object oldValue = super.put(key, value);
184             if (oldValue != null && key instanceof String) {
185                 final String keyString = (String) key;
186 
187                 duplicatedKeys.put(keyString,
188                         duplicatedKeys.getOrDefault(keyString, 0) + 1);
189             }
190             return oldValue;
191         }
192 
193         /**
194          * Retrieves a collections of duplicated properties keys.
195          *
196          * @return A collection of duplicated keys.
197          */
198         public Map<String, Integer> getDuplicatedKeys() {
199             return new HashMap<>(duplicatedKeys);
200         }
201 
202     }
203 
204 }