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;
021
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.io.Writer;
026import java.nio.charset.StandardCharsets;
027
028import com.puppycrawl.tools.checkstyle.api.AuditEvent;
029import com.puppycrawl.tools.checkstyle.api.AuditListener;
030import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
031import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
032
033/**
034 * Simple plain logger for text output.
035 * This is maybe not very suitable for a text output into a file since it
036 * does not need all 'audit finished' and so on stuff, but it looks good on
037 * stdout anyway. If there is really a problem this is what XMLLogger is for.
038 * It gives structure.
039 *
040 * @see XMLLogger
041 */
042public class DefaultLogger extends AbstractAutomaticBean implements AuditListener {
043
044    /**
045     * A key pointing to the add exception message in the "messages.properties" file.
046     */
047    public static final String ADD_EXCEPTION_MESSAGE = "DefaultLogger.addException";
048    /**
049     * A key pointing to the started audit message in the "messages.properties" file.
050     */
051    public static final String AUDIT_STARTED_MESSAGE = "DefaultLogger.auditStarted";
052    /**
053     * A key pointing to the finished audit message in the "messages.properties" file.
054     */
055    public static final String AUDIT_FINISHED_MESSAGE = "DefaultLogger.auditFinished";
056
057    /** Where to write info messages. */
058    private final PrintWriter infoWriter;
059    /** Close info stream after use. */
060    private final boolean closeInfo;
061
062    /** Where to write error messages. */
063    private final PrintWriter errorWriter;
064    /** Close error stream after use. */
065    private final boolean closeError;
066
067    /** Formatter for the log message. */
068    private final AuditEventFormatter formatter;
069
070    /**
071     * Creates a new {@code DefaultLogger} instance.
072     *
073     * @param outputStream where to log audit events
074     * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
075     * @noinspection deprecation
076     * @noinspectionreason We are forced to keep AutomaticBean compatibility
077     *     because of maven-checkstyle-plugin. Until #12873.
078     */
079    public DefaultLogger(OutputStream outputStream,
080                         AutomaticBean.OutputStreamOptions outputStreamOptions) {
081        this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
082    }
083
084    /**
085     * Creates a new {@code DefaultLogger} instance.
086     *
087     * @param outputStream where to log audit events
088     * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
089     */
090    public DefaultLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
091        // no need to close oS twice
092        this(outputStream, outputStreamOptions, outputStream, OutputStreamOptions.NONE);
093    }
094
095    /**
096     * Creates a new {@code DefaultLogger} instance.
097     *
098     * @param infoStream the {@code OutputStream} for info messages.
099     * @param infoStreamOptions if {@code CLOSE} info should be closed in auditFinished()
100     * @param errorStream the {@code OutputStream} for error messages.
101     * @param errorStreamOptions if {@code CLOSE} error should be closed in auditFinished()
102     */
103    public DefaultLogger(OutputStream infoStream,
104                         OutputStreamOptions infoStreamOptions,
105                         OutputStream errorStream,
106                         OutputStreamOptions errorStreamOptions) {
107        this(infoStream, infoStreamOptions, errorStream, errorStreamOptions,
108                new AuditEventDefaultFormatter());
109    }
110
111    /**
112     * Creates a new {@code DefaultLogger} instance.
113     *
114     * @param infoStream the {@code OutputStream} for info messages
115     * @param infoStreamOptions if {@code CLOSE} info should be closed in auditFinished()
116     * @param errorStream the {@code OutputStream} for error messages
117     * @param errorStreamOptions if {@code CLOSE} error should be closed in auditFinished()
118     * @param messageFormatter formatter for the log message.
119     * @throws IllegalArgumentException if stream options are null
120     * @noinspection WeakerAccess
121     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
122     */
123    public DefaultLogger(OutputStream infoStream,
124                         OutputStreamOptions infoStreamOptions,
125                         OutputStream errorStream,
126                         OutputStreamOptions errorStreamOptions,
127                         AuditEventFormatter messageFormatter) {
128        if (infoStreamOptions == null) {
129            throw new IllegalArgumentException(getLocalizedMessage(
130                    DefaultLogger.class,
131                    "DefaultLogger.infoStreamException"));
132        }
133        closeInfo = infoStreamOptions == OutputStreamOptions.CLOSE;
134        if (errorStreamOptions == null) {
135            throw new IllegalArgumentException(getLocalizedMessage(
136                    DefaultLogger.class,
137                    "DefaultLogger.errorStreamException"));
138        }
139        closeError = errorStreamOptions == OutputStreamOptions.CLOSE;
140        final Writer infoStreamWriter = new OutputStreamWriter(infoStream, StandardCharsets.UTF_8);
141        infoWriter = new PrintWriter(infoStreamWriter);
142
143        if (infoStream == errorStream) {
144            errorWriter = infoWriter;
145        }
146        else {
147            final Writer errorStreamWriter = new OutputStreamWriter(errorStream,
148                    StandardCharsets.UTF_8);
149            errorWriter = new PrintWriter(errorStreamWriter);
150        }
151        formatter = messageFormatter;
152    }
153
154    @Override
155    protected void finishLocalSetup() {
156        // No code by default
157    }
158
159    /**
160     * Print an Emacs compliant line on the error stream.
161     * If the column number is non-zero, then also display it.
162     *
163     * @see AuditListener
164     */
165    @Override
166    public void addError(AuditEvent event) {
167        final SeverityLevel severityLevel = event.getSeverityLevel();
168        if (severityLevel != SeverityLevel.IGNORE) {
169            final String errorMessage = formatter.format(event);
170            errorWriter.println(errorMessage);
171        }
172    }
173
174    @Override
175    public void addException(AuditEvent event, Throwable throwable) {
176        errorWriter.println(getLocalizedMessage(
177                DefaultLogger.class,
178                ADD_EXCEPTION_MESSAGE,
179                event.getFileName()));
180        throwable.printStackTrace(errorWriter);
181    }
182
183    @Override
184    public void auditStarted(AuditEvent event) {
185        infoWriter.println(getLocalizedMessage(
186                DefaultLogger.class,
187                AUDIT_STARTED_MESSAGE));
188        infoWriter.flush();
189    }
190
191    @Override
192    public void auditFinished(AuditEvent event) {
193        infoWriter.println(getLocalizedMessage(
194                DefaultLogger.class,
195                AUDIT_FINISHED_MESSAGE));
196        closeStreams();
197    }
198
199    @Override
200    public void fileStarted(AuditEvent event) {
201        // No need to implement this method in this class
202    }
203
204    @Override
205    public void fileFinished(AuditEvent event) {
206        infoWriter.flush();
207    }
208
209    /**
210     * Flushes the output streams and closes them if needed.
211     */
212    private void closeStreams() {
213        infoWriter.flush();
214        if (closeInfo) {
215            infoWriter.close();
216        }
217
218        errorWriter.flush();
219        if (closeError) {
220            errorWriter.close();
221        }
222    }
223
224    /**
225     * Extracts localized messages from properties files.
226     *
227     * @param caller the {@link Class} used to resolve the resource bundle
228     * @param messageKey the key pointing to localized message in respective properties file.
229     * @param args the arguments of message in respective properties file.
230     * @return a string containing extracted localized message
231     */
232    private static String getLocalizedMessage(Class<?> caller,
233                                              String messageKey, Object... args) {
234        final LocalizedMessage localizedMessage = new LocalizedMessage(
235            Definitions.CHECKSTYLE_BUNDLE, caller,
236                    messageKey, args);
237
238        return localizedMessage.getMessage();
239    }
240
241}