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}