001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 * <div>
041 * Detects if keys in properties files are in correct order.
042 * </div>
043 *
044 * <p>
045 *   Rationale: Sorted properties make it easy for people to find required properties by name
046 *   in file. This makes it easier to merge. While there are no problems at runtime.
047 *   This check is valuable only on files with string resources where order of lines
048 *   does not matter at all, but this can be improved.
049 *   E.g.: checkstyle/src/main/resources/com/puppycrawl/tools/checkstyle/messages.properties
050 *   You may suppress warnings of this check for files that have a logical structure like
051 *   build files or log4j configuration files. See SuppressionFilter.
052 *   {@code
053 *   &lt;suppress checks="OrderedProperties"
054 *     files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/&gt;
055 *   }
056 * </p>
057 *
058 * <p>Known limitation: The key should not contain a newline.
059 * The string compare will work, but not the line number reporting.</p>
060 * <ul><li>
061 * Property {@code fileExtensions} - Specify the file extensions of the files to process.
062 * Type is {@code java.lang.String[]}.
063 * Default value is {@code .properties}.
064 * </li></ul>
065 *
066 * <p>
067 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
068 * </p>
069 *
070 * <p>
071 * Violation Message Keys:
072 * </p>
073 * <ul>
074 * <li>
075 * {@code properties.notSorted.property}
076 * </li>
077 * <li>
078 * {@code unable.open.cause}
079 * </li>
080 * </ul>
081 *
082 * @since 8.22
083 */
084@StatelessCheck
085public class OrderedPropertiesCheck extends AbstractFileSetCheck {
086
087    /**
088     * Localization key for check violation.
089     */
090    public static final String MSG_KEY = "properties.notSorted.property";
091    /**
092     * Localization key for IO exception occurred on file open.
093     */
094    public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
095    /**
096     * Pattern matching single space.
097     */
098    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
099
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}