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