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