001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 * </p>
054 * <div class="wrapper">
055 * {@snippet lang="text" :
056 * <suppress checks="OrderedProperties"
057 *   files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/>
058 * }
059 * </div>
060 *
061 * <p>Known limitation: The key should not contain a newline.
062 * The string compare will work, but not the line number reporting.</p>
063 *
064 * @since 8.22
065 */
066@StatelessCheck
067public class OrderedPropertiesCheck extends AbstractFileSetCheck {
068
069    /**
070     * Localization key for check violation.
071     */
072    public static final String MSG_KEY = "properties.notSorted.property";
073    /**
074     * Localization key for IO exception occurred on file open.
075     */
076    public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
077    /**
078     * Pattern matching single space.
079     */
080    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
081
082    /**
083     * Compiled pattern for all system newlines.
084     */
085    private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R");
086
087    /**
088     * Construct the check with default values.
089     */
090    public OrderedPropertiesCheck() {
091        setFileExtensions("properties");
092    }
093
094    /**
095     * Setter to specify the file extensions of the files to process.
096     *
097     * @param extensions the set of file extensions. A missing
098     *         initial '.' character of an extension is automatically added.
099     * @throws IllegalArgumentException is argument is null
100     */
101    @Override
102    public final void setFileExtensions(String... extensions) {
103        super.setFileExtensions(extensions);
104    }
105
106    /**
107     * Processes the file and check order.
108     *
109     * @param file the file to be processed
110     * @param fileText the contents of the file.
111     */
112    @Override
113    protected void processFiltered(File file, FileText fileText) {
114        final SequencedProperties properties = new SequencedProperties();
115        try (InputStream inputStream = Files.newInputStream(file.toPath())) {
116            properties.load(inputStream);
117        }
118        catch (IOException | IllegalArgumentException exc) {
119            log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), exc.getLocalizedMessage());
120        }
121
122        String previousProp = "";
123        int startLineNo = 0;
124
125        final Iterator<Object> propertyIterator = properties.keys().asIterator();
126
127        while (propertyIterator.hasNext()) {
128
129            final String propKey = (String) propertyIterator.next();
130
131            if (String.CASE_INSENSITIVE_ORDER.compare(previousProp, propKey) > 0) {
132
133                final int lineNo = getLineNumber(startLineNo, fileText, previousProp, propKey);
134                log(lineNo + 1, MSG_KEY,
135                        escapeLineBreaks(propKey), escapeLineBreaks(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 of the last occurrence.
147     * Also, the previousProp should be in file before propKey.
148     *
149     * @param startLineNo start searching at line
150     * @param fileText {@link FileText} object contains the lines to process
151     * @param previousProp key name found last iteration, works only if valid
152     * @param propKey key name to look for
153     * @return index number of first occurrence. If no key found in properties file, 0 is returned
154     */
155    private static int getLineNumber(int startLineNo, FileText fileText,
156                                     String previousProp, String propKey) {
157        final int indexOfPreviousProp = getIndex(startLineNo, fileText, previousProp);
158        return getIndex(indexOfPreviousProp, fileText, propKey);
159    }
160
161    /**
162     * Inner method to get the index number of the position of keyName.
163     *
164     * @param startLineNo start searching at line
165     * @param fileText {@link FileText} object contains the lines to process
166     * @param keyName key name to look for
167     * @return index number of first occurrence. If no key found in properties file, 0 is returned
168     */
169    private static int getIndex(int startLineNo, FileText fileText, String keyName) {
170        final Pattern keyPattern = getKeyPattern(keyName);
171        int indexNumber = 0;
172        final Matcher matcher = keyPattern.matcher("");
173        for (int index = startLineNo; index < fileText.size(); index++) {
174            final String line = fileText.get(index);
175            matcher.reset(line);
176            if (matcher.matches()) {
177                indexNumber = index;
178                break;
179            }
180        }
181        return indexNumber;
182    }
183
184    /**
185     * Rewrites all line breaks in a property key as the literal {@code \n},
186     * so the violation message stays on a single line.
187     *
188     * @param keyName key name to process
189     * @return key name with all line breaks escaped
190     */
191    private static String escapeLineBreaks(String keyName) {
192        return ALL_NEW_LINES.matcher(keyName).replaceAll("\\\\n");
193    }
194
195    /**
196     * Method returns regular expression pattern given key name.
197     *
198     * @param keyName
199     *            key name to look for
200     * @return regular expression pattern given key name
201     */
202    private static Pattern getKeyPattern(String keyName) {
203        final String keyNameWithEscapedSpaces = SPACE_PATTERN.matcher(keyName)
204            .replaceAll(Matcher.quoteReplacement("\\ "));
205
206        final String keyPatternString = "^"
207            + Pattern.quote(keyNameWithEscapedSpaces)
208            + "[\\s=:].*";
209        return Pattern.compile(keyPatternString);
210    }
211
212    /**
213     * Private property implementation that keeps order of properties like in file.
214     *
215     * @noinspection ClassExtendsConcreteCollection
216     * @noinspectionreason ClassExtendsConcreteCollection - we require order from
217     *      file to be maintained by {@code put} method
218     */
219    private static final class SequencedProperties extends Properties {
220
221        /** A unique serial version identifier. */
222        @Serial
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         * Creates a new {@code SequencedProperties} instance.
232         */
233        private SequencedProperties() {
234            // no code by default
235        }
236
237        /**
238         * Returns a copy of the keys.
239         *
240         * @noinspection SynchronizedMethod
241         * @noinspectionreason SynchronizedMethod - synchronized keyword is required
242         *      to override the synchronized keys() method from Hashtable parent class,
243         *      maintaining thread-safety contract
244         */
245        @Override
246        public synchronized Enumeration<Object> keys() {
247            return Collections.enumeration(keyList);
248        }
249
250        /**
251         * Puts the value into list by its key.
252         *
253         * @param key the hashtable key
254         * @param value the value
255         * @return the previous value of the specified key in this hashtable,
256         *      or null if it did not have one
257         * @throws NullPointerException - if the key or value is null
258         */
259        @Override
260        public synchronized Object put(Object key, Object value) {
261            keyList.add(key);
262
263            return null;
264        }
265
266    }
267
268}