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