1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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   * Checks the uniqueness of property keys (left from equal sign) in the
40   * properties file.
41   *
42   */
43  @StatelessCheck
44  public class UniquePropertiesCheck extends AbstractFileSetCheck {
45  
46      /**
47       * Localization key for check violation.
48       */
49      public static final String MSG_KEY = "properties.duplicate.property";
50      /**
51       * Localization key for IO exception occurred on file open.
52       */
53      public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
54  
55      /**
56       * Pattern matching single space.
57       */
58      private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
59  
60      /**
61       * Construct the check with default values.
62       */
63      public UniquePropertiesCheck() {
64          setFileExtensions("properties");
65      }
66  
67      @Override
68      protected void processFiltered(File file, FileText fileText) {
69          final UniqueProperties properties = new UniqueProperties();
70          try (InputStream inputStream = Files.newInputStream(file.toPath())) {
71              properties.load(inputStream);
72          }
73          catch (IOException ex) {
74              log(1, MSG_IO_EXCEPTION_KEY, file.getPath(),
75                      ex.getLocalizedMessage());
76          }
77  
78          for (Entry<String, AtomicInteger> duplication : properties
79                  .getDuplicatedKeys().entrySet()) {
80              final String keyName = duplication.getKey();
81              final int lineNumber = getLineNumber(fileText, keyName);
82              // Number of occurrences is number of duplications + 1
83              log(lineNumber, MSG_KEY, keyName, duplication.getValue().get() + 1);
84          }
85      }
86  
87      /**
88       * Method returns line number the key is detected in the checked properties
89       * files first.
90       *
91       * @param fileText
92       *            {@link FileText} object contains the lines to process
93       * @param keyName
94       *            key name to look for
95       * @return line number of first occurrence. If no key found in properties
96       *         file, 1 is returned
97       */
98      private static int getLineNumber(FileText fileText, String keyName) {
99          final Pattern keyPattern = getKeyPattern(keyName);
100         int lineNumber = 1;
101         final Matcher matcher = keyPattern.matcher("");
102         for (int index = 0; index < fileText.size(); index++) {
103             final String line = fileText.get(index);
104             matcher.reset(line);
105             if (matcher.matches()) {
106                 break;
107             }
108             ++lineNumber;
109         }
110         // -1 as check seeks for the first duplicate occurrence in file,
111         // so it cannot be the last line.
112         if (lineNumber > fileText.size() - 1) {
113             lineNumber = 1;
114         }
115         return lineNumber;
116     }
117 
118     /**
119      * Method returns regular expression pattern given key name.
120      *
121      * @param keyName
122      *            key name to look for
123      * @return regular expression pattern given key name
124      */
125     private static Pattern getKeyPattern(String keyName) {
126         final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
127                 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
128         return Pattern.compile(keyPatternString);
129     }
130 
131     /**
132      * Properties subclass to store duplicated property keys in a separate map.
133      *
134      * @noinspection ClassExtendsConcreteCollection, SerializableHasSerializationMethods
135      */
136     private static class UniqueProperties extends Properties {
137 
138         private static final long serialVersionUID = 1L;
139         /**
140          * Map, holding duplicated keys and their count. Keys are added here only if they
141          * already exist in Properties' inner map.
142          */
143         private final Map<String, AtomicInteger> duplicatedKeys = new HashMap<>();
144 
145         /**
146          * Puts the value into properties by the key specified.
147          * @noinspection UseOfPropertiesAsHashtable
148          */
149         @Override
150         public synchronized Object put(Object key, Object value) {
151             final Object oldValue = super.put(key, value);
152             if (oldValue != null && key instanceof String) {
153                 final String keyString = (String) key;
154 
155                 duplicatedKeys.computeIfAbsent(keyString, empty -> new AtomicInteger(0))
156                         .incrementAndGet();
157             }
158             return oldValue;
159         }
160 
161         /**
162          * Retrieves a collections of duplicated properties keys.
163          *
164          * @return A collection of duplicated keys.
165          */
166         public Map<String, AtomicInteger> getDuplicatedKeys() {
167             return new HashMap<>(duplicatedKeys);
168         }
169 
170     }
171 
172 }