001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.regexp;
021
022import java.io.File;
023import java.io.IOException;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
028import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
029import com.puppycrawl.tools.checkstyle.api.FileText;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031
032/**
033 * <div>
034 * Checks that a specified pattern matches based on file and/or folder path.
035 * It can also be used to verify files
036 * match specific naming patterns not covered by other checks (Ex: properties,
037 * xml, etc.).
038 * </div>
039 *
040 * <p>
041 * When customizing the check, the properties are applied in a specific order.
042 * The fileExtensions property first picks only files that match any of the
043 * specific extensions supplied. Once files are matched against the
044 * fileExtensions, the match property is then used in conjunction with the
045 * patterns to determine if the check is looking for a match or mismatch on
046 * those files. If the fileNamePattern is supplied, the matching is only applied
047 * to the fileNamePattern and not the folderPattern. If no fileNamePattern is
048 * supplied, then matching is applied to the folderPattern only and will result
049 * in all files in a folder to be reported on violations. If no folderPattern is
050 * supplied, then all folders that checkstyle finds are examined for violations.
051 * The ignoreFileNameExtensions property drops the file extension and applies
052 * the fileNamePattern only to the rest of file name. For example, if the file
053 * is named 'test.java' and this property is turned on, the pattern is only
054 * applied to 'test'.
055 * </p>
056 *
057 * <p>
058 * If this check is configured with no properties, then the default behavior of
059 * this check is to report file names with spaces in them. When at least one
060 * pattern property is supplied, the entire check is under the user's control to
061 * allow them to fully customize the behavior.
062 * </p>
063 *
064 * <p>
065 * It is recommended that if you create your own pattern, to also specify a
066 * custom violation message. This allows the violation message printed to be clear what
067 * the violation is, especially if multiple RegexpOnFilename checks are used.
068 * Argument 0 for the message populates the check's folderPattern. Argument 1
069 * for the message populates the check's fileNamePattern. The file name is not
070 * passed as an argument since it is part of CheckStyle's default violation
071 * messages.
072 * </p>
073 *
074 * @since 6.15
075 */
076@StatelessCheck
077public class RegexpOnFilenameCheck extends AbstractFileSetCheck {
078
079    /**
080     * A key is pointing to the warning message text in "messages.properties"
081     * file.
082     */
083    public static final String MSG_MATCH = "regexp.filename.match";
084    /**
085     * A key is pointing to the warning message text in "messages.properties"
086     * file.
087     */
088    public static final String MSG_MISMATCH = "regexp.filename.mismatch";
089
090    /** Specify the regular expression to match the folder path against. */
091    private Pattern folderPattern;
092    /** Specify the regular expression to match the file name against. */
093    private Pattern fileNamePattern;
094    /**
095     * Control whether to look for a match or mismatch on the file name,
096     * if the fileNamePattern is supplied, otherwise it is applied on the folderPattern.
097     */
098    private boolean match = true;
099    /** Control whether to ignore the file extension for the file name match. */
100    private boolean ignoreFileNameExtensions;
101
102    /**
103     * Setter to specify the regular expression to match the folder path against.
104     *
105     * @param folderPattern format of folder.
106     * @since 6.15
107     */
108    public void setFolderPattern(Pattern folderPattern) {
109        this.folderPattern = folderPattern;
110    }
111
112    /**
113     * Setter to specify the regular expression to match the file name against.
114     *
115     * @param fileNamePattern format of file.
116     * @since 6.15
117     */
118    public void setFileNamePattern(Pattern fileNamePattern) {
119        this.fileNamePattern = fileNamePattern;
120    }
121
122    /**
123     * Setter to control whether to look for a match or mismatch on the file name,
124     * if the fileNamePattern is supplied, otherwise it is applied on the folderPattern.
125     *
126     * @param match check's option for matching file names.
127     * @since 6.15
128     */
129    public void setMatch(boolean match) {
130        this.match = match;
131    }
132
133    /**
134     * Setter to control whether to ignore the file extension for the file name match.
135     *
136     * @param ignoreFileNameExtensions check's option for ignoring file extension.
137     * @since 6.15
138     */
139    public void setIgnoreFileNameExtensions(boolean ignoreFileNameExtensions) {
140        this.ignoreFileNameExtensions = ignoreFileNameExtensions;
141    }
142
143    @Override
144    public void init() {
145        if (fileNamePattern == null && folderPattern == null) {
146            fileNamePattern = CommonUtil.createPattern("\\s");
147        }
148    }
149
150    @Override
151    protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
152        final String fileName = getFileName(file);
153        final String folderPath = getFolderPath(file);
154
155        if (isMatchFolder(folderPath) && isMatchFile(fileName)) {
156            log();
157        }
158    }
159
160    /**
161     * Retrieves the file name from the given {@code file}.
162     *
163     * @param file Input file to examine.
164     * @return The file name.
165     */
166    private String getFileName(File file) {
167        String fileName = file.getName();
168
169        if (ignoreFileNameExtensions) {
170            fileName = CommonUtil.getFileNameWithoutExtension(fileName);
171        }
172
173        return fileName;
174    }
175
176    /**
177     * Retrieves the folder path from the given {@code file}.
178     *
179     * @param file Input file to examine.
180     * @return The folder path.
181     * @throws CheckstyleException if there is an error getting the canonical
182     *         path of the {@code file}.
183     */
184    private static String getFolderPath(File file) throws CheckstyleException {
185        try {
186            return file.getCanonicalFile().getParent();
187        }
188        catch (IOException exc) {
189            throw new CheckstyleException("unable to create canonical path names for "
190                    + file.getAbsolutePath(), exc);
191        }
192    }
193
194    /**
195     * Checks if the given {@code folderPath} matches the specified
196     * {@link #folderPattern}.
197     *
198     * @param folderPath Input folder path to examine.
199     * @return true if they do match.
200     */
201    private boolean isMatchFolder(String folderPath) {
202        final boolean result;
203
204        // null pattern always matches, regardless of value of 'match'
205        if (folderPattern == null) {
206            result = true;
207        }
208        else {
209            // null pattern means 'match' applies to the folderPattern matching
210            final boolean useMatch = fileNamePattern != null || match;
211            result = folderPattern.matcher(folderPath).find() == useMatch;
212        }
213
214        return result;
215    }
216
217    /**
218     * Checks if the given {@code fileName} matches the specified
219     * {@link #fileNamePattern}.
220     *
221     * @param fileName Input file name to examine.
222     * @return true if they do match.
223     */
224    private boolean isMatchFile(String fileName) {
225        // null pattern always matches, regardless of value of 'match'
226        return fileNamePattern == null || fileNamePattern.matcher(fileName).find() == match;
227    }
228
229    /** Logs the violations for the check. */
230    private void log() {
231        final String folder = getStringOrDefault(folderPattern, "");
232        final String fileName = getStringOrDefault(fileNamePattern, "");
233
234        if (match) {
235            log(1, MSG_MATCH, folder, fileName);
236        }
237        else {
238            log(1, MSG_MISMATCH, folder, fileName);
239        }
240    }
241
242    /**
243     * Retrieves the String form of the {@code pattern} or {@code defaultString}
244     * if null.
245     *
246     * @param pattern The pattern to convert.
247     * @param defaultString The result to use if {@code pattern} is null.
248     * @return The String form of the {@code pattern}.
249     */
250    private static String getStringOrDefault(Pattern pattern, String defaultString) {
251        final String result;
252
253        if (pattern == null) {
254            result = defaultString;
255        }
256        else {
257            result = pattern.toString();
258        }
259
260        return result;
261    }
262
263}