OrderedPropertiesCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2021 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.FileText;
/**
* <p>Detects if keys in properties files are in correct order.</p>
* <p>
* Rationale: Sorted properties make it easy for people to find required properties by name
* in file. It makes merges more easy. While there are no problems at runtime.
* This check is valuable only on files with string resources where order of lines
* does not matter at all, but this can be improved.
* E.g.: checkstyle/src/main/resources/com/puppycrawl/tools/checkstyle/messages.properties
* You may suppress warnings of this check for files that have an logical structure like
* build files or log4j configuration files. See SuppressionFilter.
* {@code
* <suppress checks="OrderedProperties"
* files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/>
* }
* </p>
* <p>Known limitation: The key should not contain a newline.
* The string compare will work, but not the line number reporting.</p>
* <ul><li>
* Property {@code fileExtensions} - Specify file type extension of the files to check.
* Type is {@code java.lang.String[]}.
* Default value is {@code .properties}.
* </li></ul>
* <p>
* To configure the check:
* </p>
* <pre><module name="OrderedProperties"/></pre>
* <p>Example properties file:</p>
* <pre>
* A =65
* a =97
* key =107 than nothing
* key.sub =k is 107 and dot is 46
* key.png =value - violation
* </pre>
* <p>We check order of key's only. Here we would like to use an Locale independent
* order mechanism, an binary order. The order is case insensitive and ascending.</p>
* <ul>
* <li>The capital A is on 65 and the lowercase a is on position 97 on the ascii table.</li>
* <li>Key and key.sub are in correct order here, because only keys are relevant.
* Therefore on line 5 you have only "key" an nothing behind.
* On line 6 you have "key." The dot is on position 46 which is higher than nothing.
* key.png will reported as violation because "png" comes before "sub".</li>
* </ul>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code properties.notSorted.property}
* </li>
* <li>
* {@code unable.open.cause}
* </li>
* </ul>
*
* @since 8.22
*/
@StatelessCheck
public class OrderedPropertiesCheck extends AbstractFileSetCheck {
/**
* Localization key for check violation.
*/
public static final String MSG_KEY = "properties.notSorted.property";
/**
* Localization key for IO exception occurred on file open.
*/
public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
/**
* Pattern matching single space.
*/
private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
/**
* Construct the check with default values.
*/
public OrderedPropertiesCheck() {
setFileExtensions("properties");
}
/**
* Processes the file and check order.
*
* @param file the file to be processed
* @param fileText the contents of the file.
* @noinspection EnumerationCanBeIteration
*/
@Override
protected void processFiltered(File file, FileText fileText) {
final SequencedProperties properties = new SequencedProperties();
try (InputStream inputStream = Files.newInputStream(file.toPath())) {
properties.load(inputStream);
}
catch (IOException | IllegalArgumentException ex) {
log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), ex.getLocalizedMessage());
}
String previousProp = "";
int startLineNo = 0;
final Enumeration<Object> keys = properties.keys();
while (keys.hasMoreElements()) {
final String propKey = (String) keys.nextElement();
if (String.CASE_INSENSITIVE_ORDER.compare(previousProp, propKey) > 0) {
final int lineNo = getLineNumber(startLineNo, fileText, previousProp, propKey);
log(lineNo + 1, MSG_KEY, propKey, previousProp);
// start searching at position of the last reported validation
startLineNo = lineNo;
}
previousProp = propKey;
}
}
/**
* Method returns the index number where the key is detected (starting at 0).
* To assure that we get the correct line it starts at the point
* of the last occurrence.
* Also the previousProp should be in file before propKey.
*
* @param startLineNo start searching at line
* @param fileText {@link FileText} object contains the lines to process
* @param previousProp key name found last iteration, works only if valid
* @param propKey key name to look for
* @return index number of first occurrence. If no key found in properties file, 0 is returned
*/
private static int getLineNumber(int startLineNo, FileText fileText,
String previousProp, String propKey) {
final int indexOfPreviousProp = getIndex(startLineNo, fileText, previousProp);
return getIndex(indexOfPreviousProp, fileText, propKey);
}
/**
* Inner method to get the index number of the position of keyName.
*
* @param startLineNo start searching at line
* @param fileText {@link FileText} object contains the lines to process
* @param keyName key name to look for
* @return index number of first occurrence. If no key found in properties file, 0 is returned
*/
private static int getIndex(int startLineNo, FileText fileText, String keyName) {
final Pattern keyPattern = getKeyPattern(keyName);
int indexNumber = 0;
final Matcher matcher = keyPattern.matcher("");
for (int index = startLineNo; index < fileText.size(); index++) {
final String line = fileText.get(index);
matcher.reset(line);
if (matcher.matches()) {
indexNumber = index;
break;
}
}
return indexNumber;
}
/**
* Method returns regular expression pattern given key name.
*
* @param keyName
* key name to look for
* @return regular expression pattern given key name
*/
private static Pattern getKeyPattern(String keyName) {
final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
.replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*";
return Pattern.compile(keyPatternString);
}
/**
* Private property implementation that keeps order of properties like in file.
*
* @noinspection ClassExtendsConcreteCollection, SerializableHasSerializationMethods
*/
private static class SequencedProperties extends Properties {
private static final long serialVersionUID = 1L;
/**
* Holding the keys in the same order than in the file.
*/
private final List<Object> keyList = new ArrayList<>();
/**
* Returns a copy of the keys.
*/
@Override
public synchronized Enumeration<Object> keys() {
return Collections.enumeration(keyList);
}
/**
* Puts the value into list by its key.
*
* @noinspection UseOfPropertiesAsHashtable
*
* @param key the hashtable key
* @param value the value
* @return the previous value of the specified key in this hashtable,
* or null if it did not have one
* @throws NullPointerException - if the key or value is null
*/
@Override
public synchronized Object put(Object key, Object value) {
keyList.add(key);
return super.put(key, value);
}
}
}