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;
021
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.io.Writer;
026import java.nio.charset.StandardCharsets;
027import java.text.MessageFormat;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.Locale;
032import java.util.Map;
033import java.util.ResourceBundle;
034
035import com.puppycrawl.tools.checkstyle.api.AuditEvent;
036import com.puppycrawl.tools.checkstyle.api.AuditListener;
037import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
038import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
039import com.puppycrawl.tools.checkstyle.api.Violation;
040
041/**
042 * Simple plain logger for text output.
043 * This is maybe not very suitable for a text output into a file since it
044 * does not need all 'audit finished' and so on stuff, but it looks good on
045 * stdout anyway. If there is really a problem this is what XMLLogger is for.
046 * It gives structure.
047 *
048 * @see XMLLogger
049 */
050public class DefaultLogger extends AutomaticBean implements AuditListener {
051
052    /**
053     * A key pointing to the add exception
054     * message in the "messages.properties" file.
055     */
056    public static final String ADD_EXCEPTION_MESSAGE = "DefaultLogger.addException";
057    /**
058     * A key pointing to the started audit
059     * message in the "messages.properties" file.
060     */
061    public static final String AUDIT_STARTED_MESSAGE = "DefaultLogger.auditStarted";
062    /**
063     * A key pointing to the finished audit
064     * message in the "messages.properties" file.
065     */
066    public static final String AUDIT_FINISHED_MESSAGE = "DefaultLogger.auditFinished";
067
068    /** Where to write info messages. **/
069    private final PrintWriter infoWriter;
070    /** Close info stream after use. */
071    private final boolean closeInfo;
072
073    /** Where to write error messages. **/
074    private final PrintWriter errorWriter;
075    /** Close error stream after use. */
076    private final boolean closeError;
077
078    /** Formatter for the log message. */
079    private final AuditEventFormatter formatter;
080
081    /**
082     * Creates a new {@code DefaultLogger} instance.
083     *
084     * @param outputStream where to log audit events
085     * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
086     */
087    public DefaultLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
088        // no need to close oS twice
089        this(outputStream, outputStreamOptions, outputStream, OutputStreamOptions.NONE);
090    }
091
092    /**
093     * Creates a new {@code DefaultLogger} instance.
094     *
095     * @param infoStream the {@code OutputStream} for info messages.
096     * @param infoStreamOptions if {@code CLOSE} info should be closed in auditFinished()
097     * @param errorStream the {@code OutputStream} for error messages.
098     * @param errorStreamOptions if {@code CLOSE} error should be closed in auditFinished()
099     */
100    public DefaultLogger(OutputStream infoStream,
101                         OutputStreamOptions infoStreamOptions,
102                         OutputStream errorStream,
103                         OutputStreamOptions errorStreamOptions) {
104        this(infoStream, infoStreamOptions, errorStream, errorStreamOptions,
105                new AuditEventDefaultFormatter());
106    }
107
108    /**
109     * Creates a new {@code DefaultLogger} instance.
110     *
111     * @param infoStream the {@code OutputStream} for info messages
112     * @param infoStreamOptions if {@code CLOSE} info should be closed in auditFinished()
113     * @param errorStream the {@code OutputStream} for error messages
114     * @param errorStreamOptions if {@code CLOSE} error should be closed in auditFinished()
115     * @param messageFormatter formatter for the log message.
116     * @throws IllegalArgumentException if stream options are null
117     * @noinspection WeakerAccess
118     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
119     */
120    public DefaultLogger(OutputStream infoStream,
121                         OutputStreamOptions infoStreamOptions,
122                         OutputStream errorStream,
123                         OutputStreamOptions errorStreamOptions,
124                         AuditEventFormatter messageFormatter) {
125        if (infoStreamOptions == null) {
126            throw new IllegalArgumentException("Parameter infoStreamOptions can not be null");
127        }
128        closeInfo = infoStreamOptions == OutputStreamOptions.CLOSE;
129        if (errorStreamOptions == null) {
130            throw new IllegalArgumentException("Parameter errorStreamOptions can not be null");
131        }
132        closeError = errorStreamOptions == OutputStreamOptions.CLOSE;
133        final Writer infoStreamWriter = new OutputStreamWriter(infoStream, StandardCharsets.UTF_8);
134        infoWriter = new PrintWriter(infoStreamWriter);
135
136        if (infoStream == errorStream) {
137            errorWriter = infoWriter;
138        }
139        else {
140            final Writer errorStreamWriter = new OutputStreamWriter(errorStream,
141                    StandardCharsets.UTF_8);
142            errorWriter = new PrintWriter(errorStreamWriter);
143        }
144        formatter = messageFormatter;
145    }
146
147    @Override
148    protected void finishLocalSetup() {
149        // No code by default
150    }
151
152    /**
153     * Print an Emacs compliant line on the error stream.
154     * If the column number is non-zero, then also display it.
155     *
156     * @see AuditListener
157     **/
158    @Override
159    public void addError(AuditEvent event) {
160        final SeverityLevel severityLevel = event.getSeverityLevel();
161        if (severityLevel != SeverityLevel.IGNORE) {
162            final String errorMessage = formatter.format(event);
163            errorWriter.println(errorMessage);
164        }
165    }
166
167    @Override
168    public void addException(AuditEvent event, Throwable throwable) {
169        synchronized (errorWriter) {
170            final LocalizedMessage exceptionMessage = new LocalizedMessage(
171                    ADD_EXCEPTION_MESSAGE, event.getFileName());
172            errorWriter.println(exceptionMessage.getMessage());
173            throwable.printStackTrace(errorWriter);
174        }
175    }
176
177    @Override
178    public void auditStarted(AuditEvent event) {
179        final LocalizedMessage auditStartMessage = new LocalizedMessage(AUDIT_STARTED_MESSAGE);
180        infoWriter.println(auditStartMessage.getMessage());
181        infoWriter.flush();
182    }
183
184    @Override
185    public void auditFinished(AuditEvent event) {
186        final LocalizedMessage auditFinishMessage = new LocalizedMessage(AUDIT_FINISHED_MESSAGE);
187        infoWriter.println(auditFinishMessage.getMessage());
188        closeStreams();
189    }
190
191    @Override
192    public void fileStarted(AuditEvent event) {
193        // No need to implement this method in this class
194    }
195
196    @Override
197    public void fileFinished(AuditEvent event) {
198        infoWriter.flush();
199    }
200
201    /**
202     * Flushes the output streams and closes them if needed.
203     */
204    private void closeStreams() {
205        infoWriter.flush();
206        if (closeInfo) {
207            infoWriter.close();
208        }
209
210        errorWriter.flush();
211        if (closeError) {
212            errorWriter.close();
213        }
214    }
215
216    /**
217     * Represents a message that can be localised. The translations come from
218     * message.properties files. The underlying implementation uses
219     * java.text.MessageFormat.
220     */
221    private static final class LocalizedMessage {
222
223        /**
224         * A cache that maps bundle names to ResourceBundles.
225         * Avoids repetitive calls to ResourceBundle.getBundle().
226         */
227        private static final Map<String, ResourceBundle> BUNDLE_CACHE =
228                Collections.synchronizedMap(new HashMap<>());
229
230        /**
231         * The locale to localise messages to.
232         **/
233        private static final Locale LOCALE = Locale.getDefault();
234
235        /**
236         * Key for the message format.
237         **/
238        private final String key;
239
240        /**
241         * Arguments for MessageFormat.
242         */
243        private final String[] args;
244
245        /**
246         * Creates a new {@code LocalizedMessage} instance.
247         *
248         * @param key the key to locate the translation.
249         */
250        /* package */ LocalizedMessage(String key) {
251            this.key = key;
252            args = null;
253        }
254
255        /**
256         * Creates a new {@code LocalizedMessage} instance.
257         *
258         * @param key the key to locate the translation.
259         * @param args arguments for the translation.
260         */
261        /* package */ LocalizedMessage(String key, String... args) {
262            this.key = key;
263            if (args == null) {
264                this.args = null;
265            }
266            else {
267                this.args = Arrays.copyOf(args, args.length);
268            }
269        }
270
271        /**
272         * Gets the translated message.
273         *
274         * @return the translated message.
275         */
276        private String getMessage() {
277            // Important to use the default class loader, and not the one in
278            // the GlobalProperties object. This is because the class loader in
279            // the GlobalProperties is specified by the user for resolving
280            // custom classes.
281            final String bundle = Definitions.CHECKSTYLE_BUNDLE;
282            final ResourceBundle resourceBundle = getBundle(bundle);
283            final String pattern = resourceBundle.getString(key);
284            final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
285
286            return formatter.format(args);
287        }
288
289        /**
290         * Find a ResourceBundle for a given bundle name. Uses the classloader
291         * of the class emitting this message, to be sure to get the correct
292         * bundle.
293         *
294         * @param bundleName the bundle name.
295         * @return a ResourceBundle.
296         */
297        private static ResourceBundle getBundle(String bundleName) {
298            return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> {
299                return ResourceBundle.getBundle(
300                        name, LOCALE, LocalizedMessage.class.getClassLoader(),
301                        new Violation.Utf8Control());
302            });
303        }
304    }
305}