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.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 * {@code 054 * <suppress checks="OrderedProperties" 055 * files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/> 056 * } 057 * </p> 058 * 059 * <p>Known limitation: The key should not contain a newline. 060 * The string compare will work, but not the line number reporting.</p> 061 * <ul><li> 062 * Property {@code fileExtensions} - Specify the file extensions of the files to process. 063 * Type is {@code java.lang.String[]}. 064 * Default value is {@code .properties}. 065 * </li></ul> 066 * 067 * <p> 068 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 069 * </p> 070 * 071 * <p> 072 * Violation Message Keys: 073 * </p> 074 * <ul> 075 * <li> 076 * {@code properties.notSorted.property} 077 * </li> 078 * <li> 079 * {@code unable.open.cause} 080 * </li> 081 * </ul> 082 * 083 * @since 8.22 084 */ 085@StatelessCheck 086public class OrderedPropertiesCheck extends AbstractFileSetCheck { 087 088 /** 089 * Localization key for check violation. 090 */ 091 public static final String MSG_KEY = "properties.notSorted.property"; 092 /** 093 * Localization key for IO exception occurred on file open. 094 */ 095 public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause"; 096 /** 097 * Pattern matching single space. 098 */ 099 private static final Pattern SPACE_PATTERN = Pattern.compile(" "); 100 101 /** 102 * Construct the check with default values. 103 */ 104 public OrderedPropertiesCheck() { 105 setFileExtensions("properties"); 106 } 107 108 /** 109 * Processes the file and check order. 110 * 111 * @param file the file to be processed 112 * @param fileText the contents of the file. 113 */ 114 @Override 115 protected void processFiltered(File file, FileText fileText) { 116 final SequencedProperties properties = new SequencedProperties(); 117 try (InputStream inputStream = Files.newInputStream(file.toPath())) { 118 properties.load(inputStream); 119 } 120 catch (IOException | IllegalArgumentException exc) { 121 log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), exc.getLocalizedMessage()); 122 } 123 124 String previousProp = ""; 125 int startLineNo = 0; 126 127 final Iterator<Object> propertyIterator = properties.keys().asIterator(); 128 129 while (propertyIterator.hasNext()) { 130 131 final String propKey = (String) propertyIterator.next(); 132 133 if (String.CASE_INSENSITIVE_ORDER.compare(previousProp, propKey) > 0) { 134 135 final int lineNo = getLineNumber(startLineNo, fileText, previousProp, propKey); 136 log(lineNo + 1, MSG_KEY, propKey, previousProp); 137 // start searching at position of the last reported validation 138 startLineNo = lineNo; 139 } 140 141 previousProp = propKey; 142 } 143 } 144 145 /** 146 * Method returns the index number where the key is detected (starting at 0). 147 * To assure that we get the correct line it starts at the point 148 * of the last occurrence. 149 * Also, the previousProp should be in file before propKey. 150 * 151 * @param startLineNo start searching at line 152 * @param fileText {@link FileText} object contains the lines to process 153 * @param previousProp key name found last iteration, works only if valid 154 * @param propKey key name to look for 155 * @return index number of first occurrence. If no key found in properties file, 0 is returned 156 */ 157 private static int getLineNumber(int startLineNo, FileText fileText, 158 String previousProp, String propKey) { 159 final int indexOfPreviousProp = getIndex(startLineNo, fileText, previousProp); 160 return getIndex(indexOfPreviousProp, fileText, propKey); 161 } 162 163 /** 164 * Inner method to get the index number of the position of keyName. 165 * 166 * @param startLineNo start searching at line 167 * @param fileText {@link FileText} object contains the lines to process 168 * @param keyName key name to look for 169 * @return index number of first occurrence. If no key found in properties file, 0 is returned 170 */ 171 private static int getIndex(int startLineNo, FileText fileText, String keyName) { 172 final Pattern keyPattern = getKeyPattern(keyName); 173 int indexNumber = 0; 174 final Matcher matcher = keyPattern.matcher(""); 175 for (int index = startLineNo; index < fileText.size(); index++) { 176 final String line = fileText.get(index); 177 matcher.reset(line); 178 if (matcher.matches()) { 179 indexNumber = index; 180 break; 181 } 182 } 183 return indexNumber; 184 } 185 186 /** 187 * Method returns regular expression pattern given key name. 188 * 189 * @param keyName 190 * key name to look for 191 * @return regular expression pattern given key name 192 */ 193 private static Pattern getKeyPattern(String keyName) { 194 final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName) 195 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*"; 196 return Pattern.compile(keyPatternString); 197 } 198 199 /** 200 * Private property implementation that keeps order of properties like in file. 201 * 202 * @noinspection ClassExtendsConcreteCollection 203 * @noinspectionreason ClassExtendsConcreteCollection - we require order from 204 * file to be maintained by {@code put} method 205 */ 206 private static final class SequencedProperties extends Properties { 207 208 /** A unique serial version identifier. */ 209 @Serial 210 private static final long serialVersionUID = 1L; 211 212 /** 213 * Holding the keys in the same order as in the file. 214 */ 215 private final List<Object> keyList = new ArrayList<>(); 216 217 /** 218 * Returns a copy of the keys. 219 */ 220 @Override 221 public Enumeration<Object> keys() { 222 return Collections.enumeration(keyList); 223 } 224 225 /** 226 * Puts the value into list by its key. 227 * 228 * @param key the hashtable key 229 * @param value the value 230 * @return the previous value of the specified key in this hashtable, 231 * or null if it did not have one 232 * @throws NullPointerException - if the key or value is null 233 */ 234 @Override 235 public synchronized Object put(Object key, Object value) { 236 keyList.add(key); 237 238 return null; 239 } 240 } 241}