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