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