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.ArrayList; 27 import java.util.Collections; 28 import java.util.Enumeration; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.Properties; 32 import java.util.regex.Matcher; 33 import java.util.regex.Pattern; 34 35 import com.puppycrawl.tools.checkstyle.StatelessCheck; 36 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 37 import com.puppycrawl.tools.checkstyle.api.FileText; 38 39 /** 40 * <div> 41 * Detects if keys in properties files are in correct order. 42 * </div> 43 * 44 * <p> 45 * Rationale: Sorted properties make it easy for people to find required properties by name 46 * in file. This makes it easier to merge. While there are no problems at runtime. 47 * This check is valuable only on files with string resources where order of lines 48 * does not matter at all, but this can be improved. 49 * E.g.: checkstyle/src/main/resources/com/puppycrawl/tools/checkstyle/messages.properties 50 * You may suppress warnings of this check for files that have a logical structure like 51 * build files or log4j configuration files. See SuppressionFilter. 52 * {@code 53 * <suppress checks="OrderedProperties" 54 * files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/> 55 * } 56 * </p> 57 * 58 * <p>Known limitation: The key should not contain a newline. 59 * The string compare will work, but not the line number reporting.</p> 60 * <ul><li> 61 * Property {@code fileExtensions} - Specify the file extensions of the files to process. 62 * Type is {@code java.lang.String[]}. 63 * Default value is {@code .properties}. 64 * </li></ul> 65 * 66 * <p> 67 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 68 * </p> 69 * 70 * <p> 71 * Violation Message Keys: 72 * </p> 73 * <ul> 74 * <li> 75 * {@code properties.notSorted.property} 76 * </li> 77 * <li> 78 * {@code unable.open.cause} 79 * </li> 80 * </ul> 81 * 82 * @since 8.22 83 */ 84 @StatelessCheck 85 public class OrderedPropertiesCheck extends AbstractFileSetCheck { 86 87 /** 88 * Localization key for check violation. 89 */ 90 public static final String MSG_KEY = "properties.notSorted.property"; 91 /** 92 * Localization key for IO exception occurred on file open. 93 */ 94 public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause"; 95 /** 96 * Pattern matching single space. 97 */ 98 private static final Pattern SPACE_PATTERN = Pattern.compile(" "); 99 100 /** 101 * Construct the check with default values. 102 */ 103 public OrderedPropertiesCheck() { 104 setFileExtensions("properties"); 105 } 106 107 /** 108 * Processes the file and check order. 109 * 110 * @param file the file to be processed 111 * @param fileText the contents of the file. 112 */ 113 @Override 114 protected void processFiltered(File file, FileText fileText) { 115 final SequencedProperties properties = new SequencedProperties(); 116 try (InputStream inputStream = Files.newInputStream(file.toPath())) { 117 properties.load(inputStream); 118 } 119 catch (IOException | IllegalArgumentException ex) { 120 log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), ex.getLocalizedMessage()); 121 } 122 123 String previousProp = ""; 124 int startLineNo = 0; 125 126 final Iterator<Object> propertyIterator = properties.keys().asIterator(); 127 128 while (propertyIterator.hasNext()) { 129 130 final String propKey = (String) propertyIterator.next(); 131 132 if (String.CASE_INSENSITIVE_ORDER.compare(previousProp, propKey) > 0) { 133 134 final int lineNo = getLineNumber(startLineNo, fileText, previousProp, propKey); 135 log(lineNo + 1, MSG_KEY, propKey, 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 147 * of the last occurrence. 148 * Also, the previousProp should be in file before propKey. 149 * 150 * @param startLineNo start searching at line 151 * @param fileText {@link FileText} object contains the lines to process 152 * @param previousProp key name found last iteration, works only if valid 153 * @param propKey key name to look for 154 * @return index number of first occurrence. If no key found in properties file, 0 is returned 155 */ 156 private static int getLineNumber(int startLineNo, FileText fileText, 157 String previousProp, String propKey) { 158 final int indexOfPreviousProp = getIndex(startLineNo, fileText, previousProp); 159 return getIndex(indexOfPreviousProp, fileText, propKey); 160 } 161 162 /** 163 * Inner method to get the index number of the position of keyName. 164 * 165 * @param startLineNo start searching at line 166 * @param fileText {@link FileText} object contains the lines to process 167 * @param keyName key name to look for 168 * @return index number of first occurrence. If no key found in properties file, 0 is returned 169 */ 170 private static int getIndex(int startLineNo, FileText fileText, String keyName) { 171 final Pattern keyPattern = getKeyPattern(keyName); 172 int indexNumber = 0; 173 final Matcher matcher = keyPattern.matcher(""); 174 for (int index = startLineNo; index < fileText.size(); index++) { 175 final String line = fileText.get(index); 176 matcher.reset(line); 177 if (matcher.matches()) { 178 indexNumber = index; 179 break; 180 } 181 } 182 return indexNumber; 183 } 184 185 /** 186 * Method returns regular expression pattern given key name. 187 * 188 * @param keyName 189 * key name to look for 190 * @return regular expression pattern given key name 191 */ 192 private static Pattern getKeyPattern(String keyName) { 193 final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName) 194 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*"; 195 return Pattern.compile(keyPatternString); 196 } 197 198 /** 199 * Private property implementation that keeps order of properties like in file. 200 * 201 * @noinspection ClassExtendsConcreteCollection 202 * @noinspectionreason ClassExtendsConcreteCollection - we require order from 203 * file to be maintained by {@code put} method 204 */ 205 private static final class SequencedProperties extends Properties { 206 207 /** A unique serial version identifier. */ 208 private static final long serialVersionUID = 1L; 209 210 /** 211 * Holding the keys in the same order as in the file. 212 */ 213 private final List<Object> keyList = new ArrayList<>(); 214 215 /** 216 * Returns a copy of the keys. 217 */ 218 @Override 219 public Enumeration<Object> keys() { 220 return Collections.enumeration(keyList); 221 } 222 223 /** 224 * Puts the value into list by its key. 225 * 226 * @param key the hashtable key 227 * @param value the value 228 * @return the previous value of the specified key in this hashtable, 229 * or null if it did not have one 230 * @throws NullPointerException - if the key or value is null 231 */ 232 @Override 233 public synchronized Object put(Object key, Object value) { 234 keyList.add(key); 235 236 return null; 237 } 238 } 239 }