001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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 *   &lt;suppress checks="OrderedProperties"
051 *     files="log4j.properties|ResourceBundle/Bug.*.properties|logging.properties"/&gt;
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>&lt;module name="OrderedProperties"/&gt;</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}