001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code 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 */ 119 public DefaultLogger(OutputStream infoStream, 120 OutputStreamOptions infoStreamOptions, 121 OutputStream errorStream, 122 OutputStreamOptions errorStreamOptions, 123 AuditEventFormatter messageFormatter) { 124 if (infoStreamOptions == null) { 125 throw new IllegalArgumentException("Parameter infoStreamOptions can not be null"); 126 } 127 closeInfo = infoStreamOptions == OutputStreamOptions.CLOSE; 128 if (errorStreamOptions == null) { 129 throw new IllegalArgumentException("Parameter errorStreamOptions can not be null"); 130 } 131 closeError = errorStreamOptions == OutputStreamOptions.CLOSE; 132 final Writer infoStreamWriter = new OutputStreamWriter(infoStream, StandardCharsets.UTF_8); 133 infoWriter = new PrintWriter(infoStreamWriter); 134 135 if (infoStream == errorStream) { 136 errorWriter = infoWriter; 137 } 138 else { 139 final Writer errorStreamWriter = new OutputStreamWriter(errorStream, 140 StandardCharsets.UTF_8); 141 errorWriter = new PrintWriter(errorStreamWriter); 142 } 143 formatter = messageFormatter; 144 } 145 146 @Override 147 protected void finishLocalSetup() { 148 // No code by default 149 } 150 151 /** 152 * Print an Emacs compliant line on the error stream. 153 * If the column number is non zero, then also display it. 154 * 155 * @see AuditListener 156 **/ 157 @Override 158 public void addError(AuditEvent event) { 159 final SeverityLevel severityLevel = event.getSeverityLevel(); 160 if (severityLevel != SeverityLevel.IGNORE) { 161 final String errorMessage = formatter.format(event); 162 errorWriter.println(errorMessage); 163 } 164 } 165 166 @Override 167 public void addException(AuditEvent event, Throwable throwable) { 168 synchronized (errorWriter) { 169 final LocalizedMessage exceptionMessage = new LocalizedMessage( 170 ADD_EXCEPTION_MESSAGE, event.getFileName()); 171 errorWriter.println(exceptionMessage.getMessage()); 172 throwable.printStackTrace(errorWriter); 173 } 174 } 175 176 @Override 177 public void auditStarted(AuditEvent event) { 178 final LocalizedMessage auditStartMessage = new LocalizedMessage(AUDIT_STARTED_MESSAGE); 179 infoWriter.println(auditStartMessage.getMessage()); 180 infoWriter.flush(); 181 } 182 183 @Override 184 public void auditFinished(AuditEvent event) { 185 final LocalizedMessage auditFinishMessage = new LocalizedMessage(AUDIT_FINISHED_MESSAGE); 186 infoWriter.println(auditFinishMessage.getMessage()); 187 closeStreams(); 188 } 189 190 @Override 191 public void fileStarted(AuditEvent event) { 192 // No need to implement this method in this class 193 } 194 195 @Override 196 public void fileFinished(AuditEvent event) { 197 infoWriter.flush(); 198 } 199 200 /** 201 * Flushes the output streams and closes them if needed. 202 */ 203 private void closeStreams() { 204 infoWriter.flush(); 205 if (closeInfo) { 206 infoWriter.close(); 207 } 208 209 errorWriter.flush(); 210 if (closeError) { 211 errorWriter.close(); 212 } 213 } 214 215 /** 216 * Represents a message that can be localised. The translations come from 217 * message.properties files. The underlying implementation uses 218 * java.text.MessageFormat. 219 */ 220 private static final class LocalizedMessage { 221 222 /** 223 * A cache that maps bundle names to ResourceBundles. 224 * Avoids repetitive calls to ResourceBundle.getBundle(). 225 */ 226 private static final Map<String, ResourceBundle> BUNDLE_CACHE = 227 Collections.synchronizedMap(new HashMap<>()); 228 229 /** 230 * The locale to localise messages to. 231 **/ 232 private static final Locale LOCALE = Locale.getDefault(); 233 234 /** 235 * Key for the message format. 236 **/ 237 private final String key; 238 239 /** 240 * Arguments for MessageFormat. 241 */ 242 private final String[] args; 243 244 /** 245 * Creates a new {@code LocalizedMessage} instance. 246 * 247 * @param key the key to locate the translation. 248 */ 249 /* package */ LocalizedMessage(String key) { 250 this.key = key; 251 args = null; 252 } 253 254 /** 255 * Creates a new {@code LocalizedMessage} instance. 256 * 257 * @param key the key to locate the translation. 258 * @param args arguments for the translation. 259 */ 260 /* package */ LocalizedMessage(String key, String... args) { 261 this.key = key; 262 if (args == null) { 263 this.args = null; 264 } 265 else { 266 this.args = Arrays.copyOf(args, args.length); 267 } 268 } 269 270 /** 271 * Gets the translated message. 272 * 273 * @return the translated message. 274 */ 275 private String getMessage() { 276 // Important to use the default class loader, and not the one in 277 // the GlobalProperties object. This is because the class loader in 278 // the GlobalProperties is specified by the user for resolving 279 // custom classes. 280 final String bundle = Definitions.CHECKSTYLE_BUNDLE; 281 final ResourceBundle resourceBundle = getBundle(bundle); 282 final String pattern = resourceBundle.getString(key); 283 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 284 285 return formatter.format(args); 286 } 287 288 /** 289 * Find a ResourceBundle for a given bundle name. Uses the classloader 290 * of the class emitting this message, to be sure to get the correct 291 * bundle. 292 * 293 * @param bundleName the bundle name. 294 * @return a ResourceBundle. 295 */ 296 private static ResourceBundle getBundle(String bundleName) { 297 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> { 298 return ResourceBundle.getBundle( 299 name, LOCALE, LocalizedMessage.class.getClassLoader(), 300 new Violation.Utf8Control()); 301 }); 302 } 303 } 304}