001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.ArrayList; 028import java.util.Collections; 029import java.util.Enumeration; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Properties; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035 036import com.puppycrawl.tools.checkstyle.StatelessCheck; 037import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 038import com.puppycrawl.tools.checkstyle.api.FileText; 039 040/** 041 * <div> 042 * Detects if keys in properties files are in correct order. 043 * </div> 044 * 045 * <p> 046 * Rationale: Sorted properties make it easy for people to find required properties by name 047 * in file. This makes it easier to merge. While there are no problems at runtime. 048 * This check is valuable only on files with string resources where order of lines 049 * does not matter at all, but this can be improved. 050 * E.g.: checkstyle/src/main/resources/com/puppycrawl/tools/checkstyle/messages.properties 051 * You may suppress warnings of this check for files that have a logical structure like 052 * build files or log4j configuration files. See SuppressionFilter. 053 * </p> 054 * <div class="wrapper"> 055 * {@snippet lang="text" : 056 * <suppress checks="OrderedProperties" 057 * files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/> 058 * } 059 * </div> 060 * 061 * <p>Known limitation: The key should not contain a newline. 062 * The string compare will work, but not the line number reporting.</p> 063 * 064 * @since 8.22 065 */ 066@StatelessCheck 067public class OrderedPropertiesCheck extends AbstractFileSetCheck { 068 069 /** 070 * Localization key for check violation. 071 */ 072 public static final String MSG_KEY = "properties.notSorted.property"; 073 /** 074 * Localization key for IO exception occurred on file open. 075 */ 076 public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause"; 077 /** 078 * Pattern matching single space. 079 */ 080 private static final Pattern SPACE_PATTERN = Pattern.compile(" "); 081 082 /** 083 * Compiled pattern for all system newlines. 084 */ 085 private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R"); 086 087 /** 088 * Construct the check with default values. 089 */ 090 public OrderedPropertiesCheck() { 091 setFileExtensions("properties"); 092 } 093 094 /** 095 * Setter to specify the file extensions of the files to process. 096 * 097 * @param extensions the set of file extensions. A missing 098 * initial '.' character of an extension is automatically added. 099 * @throws IllegalArgumentException is argument is null 100 */ 101 @Override 102 public final void setFileExtensions(String... extensions) { 103 super.setFileExtensions(extensions); 104 } 105 106 /** 107 * Processes the file and check order. 108 * 109 * @param file the file to be processed 110 * @param fileText the contents of the file. 111 */ 112 @Override 113 protected void processFiltered(File file, FileText fileText) { 114 final SequencedProperties properties = new SequencedProperties(); 115 try (InputStream inputStream = Files.newInputStream(file.toPath())) { 116 properties.load(inputStream); 117 } 118 catch (IOException | IllegalArgumentException exc) { 119 log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), exc.getLocalizedMessage()); 120 } 121 122 String previousProp = ""; 123 int startLineNo = 0; 124 125 final Iterator<Object> propertyIterator = properties.keys().asIterator(); 126 127 while (propertyIterator.hasNext()) { 128 129 final String propKey = (String) propertyIterator.next(); 130 131 if (String.CASE_INSENSITIVE_ORDER.compare(previousProp, propKey) > 0) { 132 133 final int lineNo = getLineNumber(startLineNo, fileText, previousProp, propKey); 134 log(lineNo + 1, MSG_KEY, 135 escapeLineBreaks(propKey), escapeLineBreaks(previousProp)); 136 // start searching at position of the last reported validation 137 startLineNo = lineNo; 138 } 139 140 previousProp = propKey; 141 } 142 } 143 144 /** 145 * Method returns the index number where the key is detected (starting at 0). 146 * To assure that we get the correct line it starts at the point of the last occurrence. 147 * Also, the previousProp should be in file before propKey. 148 * 149 * @param startLineNo start searching at line 150 * @param fileText {@link FileText} object contains the lines to process 151 * @param previousProp key name found last iteration, works only if valid 152 * @param propKey key name to look for 153 * @return index number of first occurrence. If no key found in properties file, 0 is returned 154 */ 155 private static int getLineNumber(int startLineNo, FileText fileText, 156 String previousProp, String propKey) { 157 final int indexOfPreviousProp = getIndex(startLineNo, fileText, previousProp); 158 return getIndex(indexOfPreviousProp, fileText, propKey); 159 } 160 161 /** 162 * Inner method to get the index number of the position of keyName. 163 * 164 * @param startLineNo start searching at line 165 * @param fileText {@link FileText} object contains the lines to process 166 * @param keyName key name to look for 167 * @return index number of first occurrence. If no key found in properties file, 0 is returned 168 */ 169 private static int getIndex(int startLineNo, FileText fileText, String keyName) { 170 final Pattern keyPattern = getKeyPattern(keyName); 171 int indexNumber = 0; 172 final Matcher matcher = keyPattern.matcher(""); 173 for (int index = startLineNo; index < fileText.size(); index++) { 174 final String line = fileText.get(index); 175 matcher.reset(line); 176 if (matcher.matches()) { 177 indexNumber = index; 178 break; 179 } 180 } 181 return indexNumber; 182 } 183 184 /** 185 * Rewrites all line breaks in a property key as the literal {@code \n}, 186 * so the violation message stays on a single line. 187 * 188 * @param keyName key name to process 189 * @return key name with all line breaks escaped 190 */ 191 private static String escapeLineBreaks(String keyName) { 192 return ALL_NEW_LINES.matcher(keyName).replaceAll("\\\\n"); 193 } 194 195 /** 196 * Method returns regular expression pattern given key name. 197 * 198 * @param keyName 199 * key name to look for 200 * @return regular expression pattern given key name 201 */ 202 private static Pattern getKeyPattern(String keyName) { 203 final String keyNameWithEscapedSpaces = SPACE_PATTERN.matcher(keyName) 204 .replaceAll(Matcher.quoteReplacement("\\ ")); 205 206 final String keyPatternString = "^" 207 + Pattern.quote(keyNameWithEscapedSpaces) 208 + "[\\s=:].*"; 209 return Pattern.compile(keyPatternString); 210 } 211 212 /** 213 * Private property implementation that keeps order of properties like in file. 214 * 215 * @noinspection ClassExtendsConcreteCollection 216 * @noinspectionreason ClassExtendsConcreteCollection - we require order from 217 * file to be maintained by {@code put} method 218 */ 219 private static final class SequencedProperties extends Properties { 220 221 /** A unique serial version identifier. */ 222 @Serial 223 private static final long serialVersionUID = 1L; 224 225 /** 226 * Holding the keys in the same order as in the file. 227 */ 228 private final List<Object> keyList = new ArrayList<>(); 229 230 /** 231 * Creates a new {@code SequencedProperties} instance. 232 */ 233 private SequencedProperties() { 234 // no code by default 235 } 236 237 /** 238 * Returns a copy of the keys. 239 * 240 * @noinspection SynchronizedMethod 241 * @noinspectionreason SynchronizedMethod - synchronized keyword is required 242 * to override the synchronized keys() method from Hashtable parent class, 243 * maintaining thread-safety contract 244 */ 245 @Override 246 public synchronized Enumeration<Object> keys() { 247 return Collections.enumeration(keyList); 248 } 249 250 /** 251 * Puts the value into list by its key. 252 * 253 * @param key the hashtable key 254 * @param value the value 255 * @return the previous value of the specified key in this hashtable, 256 * or null if it did not have one 257 * @throws NullPointerException - if the key or value is null 258 */ 259 @Override 260 public synchronized Object put(Object key, Object value) { 261 keyList.add(key); 262 263 return null; 264 } 265 266 } 267 268}