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.api;
021
022import java.io.File;
023import java.util.Arrays;
024import java.util.SortedSet;
025import java.util.TreeSet;
026
027import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
028
029/**
030 * Provides common functionality for many FileSetChecks.
031 *
032 * @noinspection NoopMethodInAbstractClass
033 * @noinspectionreason NoopMethodInAbstractClass - we allow each
034 *      check to define these methods, as needed. They
035 *      should be overridden only by demand in subclasses
036 */
037public abstract class AbstractFileSetCheck
038    extends AbstractViolationReporter
039    implements FileSetCheck {
040
041    /** The extension separator. */
042    private static final String EXTENSION_SEPARATOR = ".";
043
044    /**
045     * The check context.
046     *
047     * @noinspection ThreadLocalNotStaticFinal
048     * @noinspectionreason ThreadLocalNotStaticFinal - static context is
049     *      problematic for multithreading
050     */
051    private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
052
053    /** The dispatcher errors are fired to. */
054    private MessageDispatcher messageDispatcher;
055
056    /**
057     * Specify the file extensions of the files to process.
058     * Default is uninitialized as the value is inherited from the parent module.
059     */
060    private String[] fileExtensions;
061
062    /**
063     * The tab width for column reporting.
064     * Default is uninitialized as the value is inherited from the parent module.
065     */
066    private int tabWidth;
067
068    /**
069     * Creates a new {@code AbstractFileSetCheck} instance.
070     */
071    protected AbstractFileSetCheck() {
072        // no code by default
073    }
074
075    /**
076     * Called to process a file that matches the specified file extensions.
077     *
078     * @param file the file to be processed
079     * @param fileText the contents of the file.
080     * @throws CheckstyleException if error condition within Checkstyle occurs.
081     */
082    protected abstract void processFiltered(File file, FileText fileText)
083            throws CheckstyleException;
084
085    @Override
086    public void init() {
087        // No code by default, should be overridden only by demand at subclasses
088    }
089
090    @Override
091    public void destroy() {
092        context.remove();
093    }
094
095    @Override
096    public void beginProcessing(String charset) {
097        // No code by default, should be overridden only by demand at subclasses
098    }
099
100    @Override
101    public final SortedSet<Violation> process(File file, FileText fileText)
102            throws CheckstyleException {
103        final FileContext fileContext = context.get();
104        fileContext.fileContents = new FileContents(fileText);
105        fileContext.violations.clear();
106        // Process only what interested in
107        if (CommonUtil.matchesFileExtension(file, fileExtensions)) {
108            processFiltered(file, fileText);
109        }
110        final SortedSet<Violation> result = new TreeSet<>(fileContext.violations);
111        fileContext.violations.clear();
112        return result;
113    }
114
115    @Override
116    public void finishProcessing() {
117        // No code by default, should be overridden only by demand at subclasses
118    }
119
120    @Override
121    public final void setMessageDispatcher(MessageDispatcher messageDispatcher) {
122        this.messageDispatcher = messageDispatcher;
123    }
124
125    /**
126     * A message dispatcher is used to fire violations to
127     * interested audit listeners.
128     *
129     * @return the current MessageDispatcher.
130     */
131    protected final MessageDispatcher getMessageDispatcher() {
132        return messageDispatcher;
133    }
134
135    /**
136     * Returns the sorted set of {@link Violation}.
137     *
138     * @return the sorted set of {@link Violation}.
139     */
140    public SortedSet<Violation> getViolations() {
141        return new TreeSet<>(context.get().violations);
142    }
143
144    /**
145     * Set the file contents associated with the tree.
146     *
147     * @param contents the manager
148     */
149    public final void setFileContents(FileContents contents) {
150        context.get().fileContents = contents;
151    }
152
153    /**
154     * Returns the file contents associated with the file.
155     *
156     * @return the file contents
157     */
158    protected final FileContents getFileContents() {
159        return context.get().fileContents;
160    }
161
162    /**
163     * Makes copy of file extensions and returns them.
164     *
165     * @return file extensions that identify the files that pass the
166     *     filter of this FileSetCheck.
167     */
168    public String[] getFileExtensions() {
169        return Arrays.copyOf(fileExtensions, fileExtensions.length);
170    }
171
172    /**
173     * Setter to specify the file extensions of the files to process.
174     *
175     * @param extensions the set of file extensions. A missing
176     *         initial '.' character of an extension is automatically added.
177     * @throws IllegalArgumentException is argument is null
178     */
179    public void setFileExtensions(String... extensions) {
180        if (extensions == null) {
181            throw new IllegalArgumentException("Extensions array can not be null");
182        }
183
184        fileExtensions = new String[extensions.length];
185        for (int i = 0; i < extensions.length; i++) {
186            final String extension = extensions[i];
187            if (extension.startsWith(EXTENSION_SEPARATOR)) {
188                fileExtensions[i] = extension;
189            }
190            else {
191                fileExtensions[i] = EXTENSION_SEPARATOR + extension;
192            }
193        }
194    }
195
196    /**
197     * Get tab width to report audit events with.
198     *
199     * @return the tab width to report audit events with
200     */
201    protected final int getTabWidth() {
202        return tabWidth;
203    }
204
205    /**
206     * Set the tab width to report audit events with.
207     *
208     * @param tabWidth an {@code int} value
209     */
210    public final void setTabWidth(int tabWidth) {
211        this.tabWidth = tabWidth;
212    }
213
214    /**
215     * Adds the sorted set of {@link Violation} to the message collector.
216     *
217     * @param violations the sorted set of {@link Violation}.
218     */
219    protected void addViolations(SortedSet<Violation> violations) {
220        context.get().violations.addAll(violations);
221    }
222
223    @Override
224    public final void log(int line, String key, Object... args) {
225        context.get().violations.add(
226                new Violation(line,
227                        getMessageBundle(),
228                        key,
229                        args,
230                        getSeverityLevel(),
231                        getId(),
232                        getClass(),
233                        getCustomMessages().get(key)));
234    }
235
236    @Override
237    public final void log(int lineNo, int colNo, String key,
238            Object... args) {
239        final FileContext fileContext = context.get();
240        final int col = 1 + CommonUtil.lengthExpandedTabs(
241                fileContext.fileContents.getLine(lineNo - 1), colNo, tabWidth);
242        fileContext.violations.add(
243                new Violation(lineNo,
244                        col,
245                        getMessageBundle(),
246                        key,
247                        args,
248                        getSeverityLevel(),
249                        getId(),
250                        getClass(),
251                        getCustomMessages().get(key)));
252    }
253
254    /**
255     * Notify all listeners about the errors in a file.
256     * Calls {@code MessageDispatcher.fireErrors()} with
257     * all logged errors and then clears errors' list.
258     *
259     * @param fileName the audited file
260     */
261    protected final void fireErrors(String fileName) {
262        final FileContext fileContext = context.get();
263        final SortedSet<Violation> errors = new TreeSet<>(fileContext.violations);
264        fileContext.violations.clear();
265        messageDispatcher.fireErrors(fileName, errors);
266    }
267
268    /**
269     * The actual context holder.
270     */
271    private static final class FileContext {
272        /** The sorted set for collecting violations. */
273        private final SortedSet<Violation> violations = new TreeSet<>();
274
275        /** The current file contents. */
276        private FileContents fileContents;
277
278        /**
279         * Creates a new {@code FileContext} instance.
280         */
281        private FileContext() {
282            // no code by default
283        }
284    }
285
286}