View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.concurrent.atomic.AtomicInteger;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import com.puppycrawl.tools.checkstyle.StatelessCheck;
35  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
36  import com.puppycrawl.tools.checkstyle.api.FileText;
37  
38  /**
39   * <div>
40   * Detects duplicated keys in properties files.
41   * </div>
42   *
43   * <p>
44   * Rationale: Multiple property keys usually appear after merge or rebase of
45   * several branches. While there are no problems in runtime, there can be a confusion
46   * due to having different values for the duplicated properties.
47   * </p>
48   * <ul>
49   * <li>
50   * Property {@code fileExtensions} - Specify the file extensions of the files to process.
51   * Type is {@code java.lang.String[]}.
52   * Default value is {@code .properties}.
53   * </li>
54   * </ul>
55   *
56   * <p>
57   * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
58   * </p>
59   *
60   * <p>
61   * Violation Message Keys:
62   * </p>
63   * <ul>
64   * <li>
65   * {@code properties.duplicate.property}
66   * </li>
67   * <li>
68   * {@code unable.open.cause}
69   * </li>
70   * </ul>
71   *
72   * @since 5.7
73   */
74  @StatelessCheck
75  public class UniquePropertiesCheck extends AbstractFileSetCheck {
76  
77      /**
78       * Localization key for check violation.
79       */
80      public static final String MSG_KEY = "properties.duplicate.property";
81      /**
82       * Localization key for IO exception occurred on file open.
83       */
84      public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
85  
86      /**
87       * Pattern matching single space.
88       */
89      private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
90  
91      /**
92       * Construct the check with default values.
93       */
94      public UniquePropertiesCheck() {
95          setFileExtensions("properties");
96      }
97  
98      @Override
99      protected void processFiltered(File file, FileText fileText) {
100         final UniqueProperties properties = new UniqueProperties();
101         try (InputStream inputStream = Files.newInputStream(file.toPath())) {
102             properties.load(inputStream);
103         }
104         catch (IOException ex) {
105             log(1, MSG_IO_EXCEPTION_KEY, file.getPath(),
106                     ex.getLocalizedMessage());
107         }
108 
109         for (Entry<String, AtomicInteger> duplication : properties
110                 .getDuplicatedKeys().entrySet()) {
111             final String keyName = duplication.getKey();
112             final int lineNumber = getLineNumber(fileText, keyName);
113             // Number of occurrences is number of duplications + 1
114             log(lineNumber, MSG_KEY, keyName, duplication.getValue().get() + 1);
115         }
116     }
117 
118     /**
119      * Method returns line number the key is detected in the checked properties
120      * files first.
121      *
122      * @param fileText
123      *            {@link FileText} object contains the lines to process
124      * @param keyName
125      *            key name to look for
126      * @return line number of first occurrence. If no key found in properties
127      *         file, 1 is returned
128      */
129     private static int getLineNumber(FileText fileText, String keyName) {
130         final Pattern keyPattern = getKeyPattern(keyName);
131         int lineNumber = 1;
132         final Matcher matcher = keyPattern.matcher("");
133         for (int index = 0; index < fileText.size(); index++) {
134             final String line = fileText.get(index);
135             matcher.reset(line);
136             if (matcher.matches()) {
137                 break;
138             }
139             ++lineNumber;
140         }
141         // -1 as check seeks for the first duplicate occurrence in file,
142         // so it cannot be the last line.
143         if (lineNumber > fileText.size() - 1) {
144             lineNumber = 1;
145         }
146         return lineNumber;
147     }
148 
149     /**
150      * Method returns regular expression pattern given key name.
151      *
152      * @param keyName
153      *            key name to look for
154      * @return regular expression pattern given key name
155      */
156     private static Pattern getKeyPattern(String keyName) {
157         final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
158                 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
159         return Pattern.compile(keyPatternString);
160     }
161 
162     /**
163      * Properties subclass to store duplicated property keys in a separate map.
164      *
165      * @noinspection ClassExtendsConcreteCollection
166      * @noinspectionreason ClassExtendsConcreteCollection - we require custom
167      *      {@code put} method to find duplicate keys
168      */
169     private static final class UniqueProperties extends Properties {
170 
171         /** A unique serial version identifier. */
172         private static final long serialVersionUID = 1L;
173         /**
174          * Map, holding duplicated keys and their count. Keys are added here only if they
175          * already exist in Properties' inner map.
176          */
177         private final Map<String, AtomicInteger> duplicatedKeys = new HashMap<>();
178 
179         /**
180          * Puts the value into properties by the key specified.
181          */
182         @Override
183         public synchronized Object put(Object key, Object value) {
184             final Object oldValue = super.put(key, value);
185             if (oldValue != null && key instanceof String) {
186                 final String keyString = (String) key;
187 
188                 duplicatedKeys.computeIfAbsent(keyString, empty -> new AtomicInteger(0))
189                         .incrementAndGet();
190             }
191             return oldValue;
192         }
193 
194         /**
195          * Retrieves a collections of duplicated properties keys.
196          *
197          * @return A collection of duplicated keys.
198          */
199         public Map<String, AtomicInteger> getDuplicatedKeys() {
200             return new HashMap<>(duplicatedKeys);
201         }
202 
203     }
204 
205 }