001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.IOException; 023import java.io.InputStreamReader; 024import java.io.Reader; 025import java.io.Serializable; 026import java.net.URL; 027import java.net.URLConnection; 028import java.nio.charset.StandardCharsets; 029import java.text.MessageFormat; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.Locale; 034import java.util.Map; 035import java.util.MissingResourceException; 036import java.util.Objects; 037import java.util.PropertyResourceBundle; 038import java.util.ResourceBundle; 039import java.util.ResourceBundle.Control; 040 041/** 042 * Represents a message that can be localised. The translations come from 043 * message.properties files. The underlying implementation uses 044 * java.text.MessageFormat. 045 * 046 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors 047 */ 048public final class LocalizedMessage 049 implements Comparable<LocalizedMessage>, Serializable { 050 051 private static final long serialVersionUID = 5675176836184862150L; 052 053 /** 054 * A cache that maps bundle names to ResourceBundles. 055 * Avoids repetitive calls to ResourceBundle.getBundle(). 056 */ 057 private static final Map<String, ResourceBundle> BUNDLE_CACHE = 058 Collections.synchronizedMap(new HashMap<>()); 059 060 /** The default severity level if one is not specified. */ 061 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR; 062 063 /** The locale to localise messages to. **/ 064 private static Locale sLocale = Locale.getDefault(); 065 066 /** The line number. **/ 067 private final int lineNo; 068 /** The column number. **/ 069 private final int columnNo; 070 /** The column char index. **/ 071 private final int columnCharIndex; 072 /** The token type constant. See {@link TokenTypes}. **/ 073 private final int tokenType; 074 075 /** The severity level. **/ 076 private final SeverityLevel severityLevel; 077 078 /** The id of the module generating the message. */ 079 private final String moduleId; 080 081 /** Key for the message format. **/ 082 private final String key; 083 084 /** Arguments for MessageFormat. 085 * @noinspection NonSerializableFieldInSerializableClass 086 */ 087 private final Object[] args; 088 089 /** Name of the resource bundle to get messages from. **/ 090 private final String bundle; 091 092 /** Class of the source for this LocalizedMessage. */ 093 private final Class<?> sourceClass; 094 095 /** A custom message overriding the default message from the bundle. */ 096 private final String customMessage; 097 098 /** 099 * Creates a new {@code LocalizedMessage} instance. 100 * 101 * @param lineNo line number associated with the message 102 * @param columnNo column number associated with the message 103 * @param columnCharIndex column char index associated with the message 104 * @param tokenType token type of the event associated with the message. See {@link TokenTypes} 105 * @param bundle resource bundle name 106 * @param key the key to locate the translation 107 * @param args arguments for the translation 108 * @param severityLevel severity level for the message 109 * @param moduleId the id of the module the message is associated with 110 * @param sourceClass the Class that is the source of the message 111 * @param customMessage optional custom message overriding the default 112 * @noinspection ConstructorWithTooManyParameters 113 */ 114 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 115 public LocalizedMessage(int lineNo, 116 int columnNo, 117 int columnCharIndex, 118 int tokenType, 119 String bundle, 120 String key, 121 Object[] args, 122 SeverityLevel severityLevel, 123 String moduleId, 124 Class<?> sourceClass, 125 String customMessage) { 126 this.lineNo = lineNo; 127 this.columnNo = columnNo; 128 this.columnCharIndex = columnCharIndex; 129 this.tokenType = tokenType; 130 this.key = key; 131 132 if (args == null) { 133 this.args = null; 134 } 135 else { 136 this.args = Arrays.copyOf(args, args.length); 137 } 138 this.bundle = bundle; 139 this.severityLevel = severityLevel; 140 this.moduleId = moduleId; 141 this.sourceClass = sourceClass; 142 this.customMessage = customMessage; 143 } 144 145 /** 146 * Creates a new {@code LocalizedMessage} instance. 147 * 148 * @param lineNo line number associated with the message 149 * @param columnNo column number associated with the message 150 * @param tokenType token type of the event associated with the message. See {@link TokenTypes} 151 * @param bundle resource bundle name 152 * @param key the key to locate the translation 153 * @param args arguments for the translation 154 * @param severityLevel severity level for the message 155 * @param moduleId the id of the module the message is associated with 156 * @param sourceClass the Class that is the source of the message 157 * @param customMessage optional custom message overriding the default 158 * @noinspection ConstructorWithTooManyParameters 159 */ 160 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 161 public LocalizedMessage(int lineNo, 162 int columnNo, 163 int tokenType, 164 String bundle, 165 String key, 166 Object[] args, 167 SeverityLevel severityLevel, 168 String moduleId, 169 Class<?> sourceClass, 170 String customMessage) { 171 this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId, 172 sourceClass, customMessage); 173 } 174 175 /** 176 * Creates a new {@code LocalizedMessage} instance. 177 * 178 * @param lineNo line number associated with the message 179 * @param columnNo column number associated with the message 180 * @param bundle resource bundle name 181 * @param key the key to locate the translation 182 * @param args arguments for the translation 183 * @param severityLevel severity level for the message 184 * @param moduleId the id of the module the message is associated with 185 * @param sourceClass the Class that is the source of the message 186 * @param customMessage optional custom message overriding the default 187 * @noinspection ConstructorWithTooManyParameters 188 */ 189 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 190 public LocalizedMessage(int lineNo, 191 int columnNo, 192 String bundle, 193 String key, 194 Object[] args, 195 SeverityLevel severityLevel, 196 String moduleId, 197 Class<?> sourceClass, 198 String customMessage) { 199 this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass, 200 customMessage); 201 } 202 203 /** 204 * Creates a new {@code LocalizedMessage} instance. 205 * 206 * @param lineNo line number associated with the message 207 * @param columnNo column number associated with the message 208 * @param bundle resource bundle name 209 * @param key the key to locate the translation 210 * @param args arguments for the translation 211 * @param moduleId the id of the module the message is associated with 212 * @param sourceClass the Class that is the source of the message 213 * @param customMessage optional custom message overriding the default 214 * @noinspection ConstructorWithTooManyParameters 215 */ 216 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 217 public LocalizedMessage(int lineNo, 218 int columnNo, 219 String bundle, 220 String key, 221 Object[] args, 222 String moduleId, 223 Class<?> sourceClass, 224 String customMessage) { 225 this(lineNo, 226 columnNo, 227 bundle, 228 key, 229 args, 230 DEFAULT_SEVERITY, 231 moduleId, 232 sourceClass, 233 customMessage); 234 } 235 236 /** 237 * Creates a new {@code LocalizedMessage} instance. 238 * 239 * @param lineNo line number associated with the message 240 * @param bundle resource bundle name 241 * @param key the key to locate the translation 242 * @param args arguments for the translation 243 * @param severityLevel severity level for the message 244 * @param moduleId the id of the module the message is associated with 245 * @param sourceClass the source class for the message 246 * @param customMessage optional custom message overriding the default 247 * @noinspection ConstructorWithTooManyParameters 248 */ 249 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 250 public LocalizedMessage(int lineNo, 251 String bundle, 252 String key, 253 Object[] args, 254 SeverityLevel severityLevel, 255 String moduleId, 256 Class<?> sourceClass, 257 String customMessage) { 258 this(lineNo, 0, bundle, key, args, severityLevel, moduleId, 259 sourceClass, customMessage); 260 } 261 262 /** 263 * Creates a new {@code LocalizedMessage} instance. The column number 264 * defaults to 0. 265 * 266 * @param lineNo line number associated with the message 267 * @param bundle name of a resource bundle that contains error messages 268 * @param key the key to locate the translation 269 * @param args arguments for the translation 270 * @param moduleId the id of the module the message is associated with 271 * @param sourceClass the name of the source for the message 272 * @param customMessage optional custom message overriding the default 273 */ 274 public LocalizedMessage( 275 int lineNo, 276 String bundle, 277 String key, 278 Object[] args, 279 String moduleId, 280 Class<?> sourceClass, 281 String customMessage) { 282 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId, 283 sourceClass, customMessage); 284 } 285 286 /** 287 * Indicates whether some other object is "equal to" this one. 288 * Suppression on enumeration is needed so code stays consistent. 289 * @noinspection EqualsCalledOnEnumConstant 290 */ 291 // -@cs[CyclomaticComplexity] equals - a lot of fields to check. 292 @Override 293 public boolean equals(Object object) { 294 if (this == object) { 295 return true; 296 } 297 if (object == null || getClass() != object.getClass()) { 298 return false; 299 } 300 final LocalizedMessage localizedMessage = (LocalizedMessage) object; 301 return Objects.equals(lineNo, localizedMessage.lineNo) 302 && Objects.equals(columnNo, localizedMessage.columnNo) 303 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex) 304 && Objects.equals(tokenType, localizedMessage.tokenType) 305 && Objects.equals(severityLevel, localizedMessage.severityLevel) 306 && Objects.equals(moduleId, localizedMessage.moduleId) 307 && Objects.equals(key, localizedMessage.key) 308 && Objects.equals(bundle, localizedMessage.bundle) 309 && Objects.equals(sourceClass, localizedMessage.sourceClass) 310 && Objects.equals(customMessage, localizedMessage.customMessage) 311 && Arrays.equals(args, localizedMessage.args); 312 } 313 314 @Override 315 public int hashCode() { 316 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId, 317 key, bundle, sourceClass, customMessage, Arrays.hashCode(args)); 318 } 319 320 /** Clears the cache. */ 321 public static void clearCache() { 322 BUNDLE_CACHE.clear(); 323 } 324 325 /** 326 * Gets the translated message. 327 * @return the translated message 328 */ 329 public String getMessage() { 330 String message = getCustomMessage(); 331 332 if (message == null) { 333 try { 334 // Important to use the default class loader, and not the one in 335 // the GlobalProperties object. This is because the class loader in 336 // the GlobalProperties is specified by the user for resolving 337 // custom classes. 338 final ResourceBundle resourceBundle = getBundle(bundle); 339 final String pattern = resourceBundle.getString(key); 340 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 341 message = formatter.format(args); 342 } 343 catch (final MissingResourceException ignored) { 344 // If the Check author didn't provide i18n resource bundles 345 // and logs error messages directly, this will return 346 // the author's original message 347 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); 348 message = formatter.format(args); 349 } 350 } 351 return message; 352 } 353 354 /** 355 * Returns the formatted custom message if one is configured. 356 * @return the formatted custom message or {@code null} 357 * if there is no custom message 358 */ 359 private String getCustomMessage() { 360 String message = null; 361 if (customMessage != null) { 362 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); 363 message = formatter.format(args); 364 } 365 return message; 366 } 367 368 /** 369 * Find a ResourceBundle for a given bundle name. Uses the classloader 370 * of the class emitting this message, to be sure to get the correct 371 * bundle. 372 * @param bundleName the bundle name 373 * @return a ResourceBundle 374 */ 375 private ResourceBundle getBundle(String bundleName) { 376 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> ResourceBundle.getBundle( 377 name, sLocale, sourceClass.getClassLoader(), new Utf8Control())); 378 } 379 380 /** 381 * Gets the line number. 382 * @return the line number 383 */ 384 public int getLineNo() { 385 return lineNo; 386 } 387 388 /** 389 * Gets the column number. 390 * @return the column number 391 */ 392 public int getColumnNo() { 393 return columnNo; 394 } 395 396 /** 397 * Gets the column char index. 398 * @return the column char index 399 */ 400 public int getColumnCharIndex() { 401 return columnCharIndex; 402 } 403 404 /** 405 * Gets the token type. 406 * @return the token type 407 */ 408 public int getTokenType() { 409 return tokenType; 410 } 411 412 /** 413 * Gets the severity level. 414 * @return the severity level 415 */ 416 public SeverityLevel getSeverityLevel() { 417 return severityLevel; 418 } 419 420 /** 421 * Returns id of module. 422 * @return the module identifier. 423 */ 424 public String getModuleId() { 425 return moduleId; 426 } 427 428 /** 429 * Returns the message key to locate the translation, can also be used 430 * in IDE plugins to map error messages to corrective actions. 431 * 432 * @return the message key 433 */ 434 public String getKey() { 435 return key; 436 } 437 438 /** 439 * Gets the name of the source for this LocalizedMessage. 440 * @return the name of the source for this LocalizedMessage 441 */ 442 public String getSourceName() { 443 return sourceClass.getName(); 444 } 445 446 /** 447 * Sets a locale to use for localization. 448 * @param locale the locale to use for localization 449 */ 450 public static void setLocale(Locale locale) { 451 clearCache(); 452 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 453 sLocale = Locale.ROOT; 454 } 455 else { 456 sLocale = locale; 457 } 458 } 459 460 //////////////////////////////////////////////////////////////////////////// 461 // Interface Comparable methods 462 //////////////////////////////////////////////////////////////////////////// 463 464 @Override 465 public int compareTo(LocalizedMessage other) { 466 final int result; 467 468 if (lineNo == other.lineNo) { 469 if (columnNo == other.columnNo) { 470 if (Objects.equals(moduleId, other.moduleId)) { 471 result = getMessage().compareTo(other.getMessage()); 472 } 473 else if (moduleId == null) { 474 result = -1; 475 } 476 else if (other.moduleId == null) { 477 result = 1; 478 } 479 else { 480 result = moduleId.compareTo(other.moduleId); 481 } 482 } 483 else { 484 result = Integer.compare(columnNo, other.columnNo); 485 } 486 } 487 else { 488 result = Integer.compare(lineNo, other.lineNo); 489 } 490 return result; 491 } 492 493 /** 494 * <p> 495 * Custom ResourceBundle.Control implementation which allows explicitly read 496 * the properties files as UTF-8. 497 * </p> 498 */ 499 public static class Utf8Control extends Control { 500 501 @Override 502 public ResourceBundle newBundle(String baseName, Locale locale, String format, 503 ClassLoader loader, boolean reload) throws IOException { 504 // The below is a copy of the default implementation. 505 final String bundleName = toBundleName(baseName, locale); 506 final String resourceName = toResourceName(bundleName, "properties"); 507 final URL url = loader.getResource(resourceName); 508 ResourceBundle resourceBundle = null; 509 if (url != null) { 510 final URLConnection connection = url.openConnection(); 511 if (connection != null) { 512 connection.setUseCaches(!reload); 513 try (Reader streamReader = new InputStreamReader(connection.getInputStream(), 514 StandardCharsets.UTF_8.name())) { 515 // Only this line is changed to make it read property files as UTF-8. 516 resourceBundle = new PropertyResourceBundle(streamReader); 517 } 518 } 519 } 520 return resourceBundle; 521 } 522 523 } 524 525}