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 /** 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 * Indicates whether some other object is "equal to" this one. 290 * Suppression on enumeration is needed so code stays consistent. 291 * @noinspection EqualsCalledOnEnumConstant 292 */ 293 // -@cs[CyclomaticComplexity] equals - a lot of fields to check. 294 @Override 295 public boolean equals(Object object) { 296 if (this == object) { 297 return true; 298 } 299 if (object == null || getClass() != object.getClass()) { 300 return false; 301 } 302 final LocalizedMessage localizedMessage = (LocalizedMessage) object; 303 return Objects.equals(lineNo, localizedMessage.lineNo) 304 && Objects.equals(columnNo, localizedMessage.columnNo) 305 && Objects.equals(columnCharIndex, localizedMessage.columnCharIndex) 306 && Objects.equals(tokenType, localizedMessage.tokenType) 307 && Objects.equals(severityLevel, localizedMessage.severityLevel) 308 && Objects.equals(moduleId, localizedMessage.moduleId) 309 && Objects.equals(key, localizedMessage.key) 310 && Objects.equals(bundle, localizedMessage.bundle) 311 && Objects.equals(sourceClass, localizedMessage.sourceClass) 312 && Objects.equals(customMessage, localizedMessage.customMessage) 313 && Arrays.equals(args, localizedMessage.args); 314 } 315 316 @Override 317 public int hashCode() { 318 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId, 319 key, bundle, sourceClass, customMessage, Arrays.hashCode(args)); 320 } 321 322 /** Clears the cache. */ 323 public static void clearCache() { 324 BUNDLE_CACHE.clear(); 325 } 326 327 /** 328 * Gets the translated message. 329 * @return the translated message 330 */ 331 public String getMessage() { 332 String message = getCustomMessage(); 333 334 if (message == null) { 335 try { 336 // Important to use the default class loader, and not the one in 337 // the GlobalProperties object. This is because the class loader in 338 // the GlobalProperties is specified by the user for resolving 339 // custom classes. 340 final ResourceBundle resourceBundle = getBundle(bundle); 341 final String pattern = resourceBundle.getString(key); 342 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 343 message = formatter.format(args); 344 } 345 catch (final MissingResourceException ignored) { 346 // If the Check author didn't provide i18n resource bundles 347 // and logs audit event messages directly, this will return 348 // the author's original message 349 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); 350 message = formatter.format(args); 351 } 352 } 353 return message; 354 } 355 356 /** 357 * Returns the formatted custom message if one is configured. 358 * @return the formatted custom message or {@code null} 359 * if there is no custom message 360 */ 361 private String getCustomMessage() { 362 String message = null; 363 if (customMessage != null) { 364 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); 365 message = formatter.format(args); 366 } 367 return message; 368 } 369 370 /** 371 * Find a ResourceBundle for a given bundle name. Uses the classloader 372 * of the class emitting this message, to be sure to get the correct 373 * bundle. 374 * @param bundleName the bundle name 375 * @return a ResourceBundle 376 */ 377 private ResourceBundle getBundle(String bundleName) { 378 return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> { 379 return ResourceBundle.getBundle( 380 name, sLocale, sourceClass.getClassLoader(), new Utf8Control()); 381 }); 382 } 383 384 /** 385 * Gets the line number. 386 * @return the line number 387 */ 388 public int getLineNo() { 389 return lineNo; 390 } 391 392 /** 393 * Gets the column number. 394 * @return the column number 395 */ 396 public int getColumnNo() { 397 return columnNo; 398 } 399 400 /** 401 * Gets the column char index. 402 * @return the column char index 403 */ 404 public int getColumnCharIndex() { 405 return columnCharIndex; 406 } 407 408 /** 409 * Gets the token type. 410 * @return the token type 411 */ 412 public int getTokenType() { 413 return tokenType; 414 } 415 416 /** 417 * Gets the severity level. 418 * @return the severity level 419 */ 420 public SeverityLevel getSeverityLevel() { 421 return severityLevel; 422 } 423 424 /** 425 * Returns id of module. 426 * @return the module identifier. 427 */ 428 public String getModuleId() { 429 return moduleId; 430 } 431 432 /** 433 * Returns the message key to locate the translation, can also be used 434 * in IDE plugins to map audit event messages to corrective actions. 435 * 436 * @return the message key 437 */ 438 public String getKey() { 439 return key; 440 } 441 442 /** 443 * Gets the name of the source for this LocalizedMessage. 444 * @return the name of the source for this LocalizedMessage 445 */ 446 public String getSourceName() { 447 return sourceClass.getName(); 448 } 449 450 /** 451 * Sets a locale to use for localization. 452 * @param locale the locale to use for localization 453 */ 454 public static void setLocale(Locale locale) { 455 clearCache(); 456 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 457 sLocale = Locale.ROOT; 458 } 459 else { 460 sLocale = locale; 461 } 462 } 463 464 //////////////////////////////////////////////////////////////////////////// 465 // Interface Comparable methods 466 //////////////////////////////////////////////////////////////////////////// 467 468 @Override 469 public int compareTo(LocalizedMessage other) { 470 final int result; 471 472 if (lineNo == other.lineNo) { 473 if (columnNo == other.columnNo) { 474 if (Objects.equals(moduleId, other.moduleId)) { 475 result = getMessage().compareTo(other.getMessage()); 476 } 477 else if (moduleId == null) { 478 result = -1; 479 } 480 else if (other.moduleId == null) { 481 result = 1; 482 } 483 else { 484 result = moduleId.compareTo(other.moduleId); 485 } 486 } 487 else { 488 result = Integer.compare(columnNo, other.columnNo); 489 } 490 } 491 else { 492 result = Integer.compare(lineNo, other.lineNo); 493 } 494 return result; 495 } 496 497 /** 498 * <p> 499 * Custom ResourceBundle.Control implementation which allows explicitly read 500 * the properties files as UTF-8. 501 * </p> 502 */ 503 public static class Utf8Control extends Control { 504 505 @Override 506 public ResourceBundle newBundle(String baseName, Locale locale, String format, 507 ClassLoader loader, boolean reload) throws IOException { 508 // The below is a copy of the default implementation. 509 final String bundleName = toBundleName(baseName, locale); 510 final String resourceName = toResourceName(bundleName, "properties"); 511 final URL url = loader.getResource(resourceName); 512 ResourceBundle resourceBundle = null; 513 if (url != null) { 514 final URLConnection connection = url.openConnection(); 515 if (connection != null) { 516 connection.setUseCaches(!reload); 517 try (Reader streamReader = new InputStreamReader(connection.getInputStream(), 518 StandardCharsets.UTF_8.name())) { 519 // Only this line is changed to make it read property files as UTF-8. 520 resourceBundle = new PropertyResourceBundle(streamReader); 521 } 522 } 523 } 524 return resourceBundle; 525 } 526 527 } 528 529}