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