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