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