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 */ 049public final class LocalizedMessage 050 implements Comparable<LocalizedMessage>, Serializable { 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 071 /** The severity level. **/ 072 private final SeverityLevel severityLevel; 073 074 /** The id of the module generating the message. */ 075 private final String moduleId; 076 077 /** Key for the message format. **/ 078 private final String key; 079 080 /** Arguments for MessageFormat. **/ 081 private final Object[] args; 082 083 /** Name of the resource bundle to get messages from. **/ 084 private final String bundle; 085 086 /** Class of the source for this LocalizedMessage. */ 087 private final Class<?> sourceClass; 088 089 /** A custom message overriding the default message from the bundle. */ 090 private final String customMessage; 091 092 /** 093 * Creates a new {@code LocalizedMessage} instance. 094 * 095 * @param lineNo line number associated with the message 096 * @param columnNo column number associated with the message 097 * @param bundle resource bundle name 098 * @param key the key to locate the translation 099 * @param args arguments for the translation 100 * @param severityLevel severity level for the message 101 * @param moduleId the id of the module the message is associated with 102 * @param sourceClass the Class that is the source of the message 103 * @param customMessage optional custom message overriding the default 104 */ 105 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 106 public LocalizedMessage(int lineNo, 107 int columnNo, 108 String bundle, 109 String key, 110 Object[] args, 111 SeverityLevel severityLevel, 112 String moduleId, 113 Class<?> sourceClass, 114 String customMessage) { 115 this.lineNo = lineNo; 116 this.columnNo = columnNo; 117 this.key = key; 118 119 if (args == null) { 120 this.args = null; 121 } 122 else { 123 this.args = Arrays.copyOf(args, args.length); 124 } 125 this.bundle = bundle; 126 this.severityLevel = severityLevel; 127 this.moduleId = moduleId; 128 this.sourceClass = sourceClass; 129 this.customMessage = customMessage; 130 } 131 132 /** 133 * Creates a new {@code LocalizedMessage} instance. 134 * 135 * @param lineNo line number associated with the message 136 * @param columnNo column number associated with the message 137 * @param bundle resource bundle name 138 * @param key the key to locate the translation 139 * @param args arguments for the translation 140 * @param moduleId the id of the module the message is associated with 141 * @param sourceClass the Class that is the source of the message 142 * @param customMessage optional custom message overriding the default 143 */ 144 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 145 public LocalizedMessage(int lineNo, 146 int columnNo, 147 String bundle, 148 String key, 149 Object[] args, 150 String moduleId, 151 Class<?> sourceClass, 152 String customMessage) { 153 this(lineNo, 154 columnNo, 155 bundle, 156 key, 157 args, 158 DEFAULT_SEVERITY, 159 moduleId, 160 sourceClass, 161 customMessage); 162 } 163 164 /** 165 * Creates a new {@code LocalizedMessage} instance. 166 * 167 * @param lineNo line number associated with the message 168 * @param bundle resource bundle name 169 * @param key the key to locate the translation 170 * @param args arguments for the translation 171 * @param severityLevel severity level for the message 172 * @param moduleId the id of the module the message is associated with 173 * @param sourceClass the source class for the message 174 * @param customMessage optional custom message overriding the default 175 */ 176 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments. 177 public LocalizedMessage(int lineNo, 178 String bundle, 179 String key, 180 Object[] args, 181 SeverityLevel severityLevel, 182 String moduleId, 183 Class<?> sourceClass, 184 String customMessage) { 185 this(lineNo, 0, bundle, key, args, severityLevel, moduleId, 186 sourceClass, customMessage); 187 } 188 189 /** 190 * Creates a new {@code LocalizedMessage} instance. The column number 191 * defaults to 0. 192 * 193 * @param lineNo line number associated with the message 194 * @param bundle name of a resource bundle that contains error messages 195 * @param key the key to locate the translation 196 * @param args arguments for the translation 197 * @param moduleId the id of the module the message is associated with 198 * @param sourceClass the name of the source for the message 199 * @param customMessage optional custom message overriding the default 200 */ 201 public LocalizedMessage( 202 int lineNo, 203 String bundle, 204 String key, 205 Object[] args, 206 String moduleId, 207 Class<?> sourceClass, 208 String customMessage) { 209 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId, 210 sourceClass, customMessage); 211 } 212 213 // -@cs[CyclomaticComplexity] equals - a lot of fields to check. 214 @Override 215 public boolean equals(Object object) { 216 if (this == object) { 217 return true; 218 } 219 if (object == null || getClass() != object.getClass()) { 220 return false; 221 } 222 final LocalizedMessage localizedMessage = (LocalizedMessage) object; 223 return Objects.equals(lineNo, localizedMessage.lineNo) 224 && Objects.equals(columnNo, localizedMessage.columnNo) 225 && Objects.equals(severityLevel, localizedMessage.severityLevel) 226 && Objects.equals(moduleId, localizedMessage.moduleId) 227 && Objects.equals(key, localizedMessage.key) 228 && Objects.equals(bundle, localizedMessage.bundle) 229 && Objects.equals(sourceClass, localizedMessage.sourceClass) 230 && Objects.equals(customMessage, localizedMessage.customMessage) 231 && Arrays.equals(args, localizedMessage.args); 232 } 233 234 @Override 235 public int hashCode() { 236 return Objects.hash(lineNo, columnNo, severityLevel, moduleId, key, bundle, sourceClass, 237 customMessage, Arrays.hashCode(args)); 238 } 239 240 /** Clears the cache. */ 241 public static void clearCache() { 242 synchronized (BUNDLE_CACHE) { 243 BUNDLE_CACHE.clear(); 244 } 245 } 246 247 /** 248 * Gets the translated message. 249 * @return the translated message 250 */ 251 public String getMessage() { 252 String message = getCustomMessage(); 253 254 if (message == null) { 255 try { 256 // Important to use the default class loader, and not the one in 257 // the GlobalProperties object. This is because the class loader in 258 // the GlobalProperties is specified by the user for resolving 259 // custom classes. 260 final ResourceBundle resourceBundle = getBundle(bundle); 261 final String pattern = resourceBundle.getString(key); 262 final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT); 263 message = formatter.format(args); 264 } 265 catch (final MissingResourceException ignored) { 266 // If the Check author didn't provide i18n resource bundles 267 // and logs error messages directly, this will return 268 // the author's original message 269 final MessageFormat formatter = new MessageFormat(key, Locale.ROOT); 270 message = formatter.format(args); 271 } 272 } 273 return message; 274 } 275 276 /** 277 * Returns the formatted custom message if one is configured. 278 * @return the formatted custom message or {@code null} 279 * if there is no custom message 280 */ 281 private String getCustomMessage() { 282 String message = null; 283 if (customMessage != null) { 284 final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT); 285 message = formatter.format(args); 286 } 287 return message; 288 } 289 290 /** 291 * Find a ResourceBundle for a given bundle name. Uses the classloader 292 * of the class emitting this message, to be sure to get the correct 293 * bundle. 294 * @param bundleName the bundle name 295 * @return a ResourceBundle 296 */ 297 private ResourceBundle getBundle(String bundleName) { 298 synchronized (BUNDLE_CACHE) { 299 ResourceBundle resourceBundle = BUNDLE_CACHE 300 .get(bundleName); 301 if (resourceBundle == null) { 302 resourceBundle = ResourceBundle.getBundle(bundleName, sLocale, 303 sourceClass.getClassLoader(), new Utf8Control()); 304 BUNDLE_CACHE.put(bundleName, resourceBundle); 305 } 306 return resourceBundle; 307 } 308 } 309 310 /** 311 * Gets the line number. 312 * @return the line number 313 */ 314 public int getLineNo() { 315 return lineNo; 316 } 317 318 /** 319 * Gets the column number. 320 * @return the column number 321 */ 322 public int getColumnNo() { 323 return columnNo; 324 } 325 326 /** 327 * Gets the severity level. 328 * @return the severity level 329 */ 330 public SeverityLevel getSeverityLevel() { 331 return severityLevel; 332 } 333 334 /** 335 * Returns id of module. 336 * @return the module identifier. 337 */ 338 public String getModuleId() { 339 return moduleId; 340 } 341 342 /** 343 * Returns the message key to locate the translation, can also be used 344 * in IDE plugins to map error messages to corrective actions. 345 * 346 * @return the message key 347 */ 348 public String getKey() { 349 return key; 350 } 351 352 /** 353 * Gets the name of the source for this LocalizedMessage. 354 * @return the name of the source for this LocalizedMessage 355 */ 356 public String getSourceName() { 357 return sourceClass.getName(); 358 } 359 360 /** 361 * Sets a locale to use for localization. 362 * @param locale the locale to use for localization 363 */ 364 public static void setLocale(Locale locale) { 365 clearCache(); 366 if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 367 sLocale = Locale.ROOT; 368 } 369 else { 370 sLocale = locale; 371 } 372 } 373 374 //////////////////////////////////////////////////////////////////////////// 375 // Interface Comparable methods 376 //////////////////////////////////////////////////////////////////////////// 377 378 @Override 379 public int compareTo(LocalizedMessage other) { 380 int result = Integer.compare(lineNo, other.lineNo); 381 382 if (lineNo == other.lineNo) { 383 if (columnNo == other.columnNo) { 384 result = getMessage().compareTo(other.getMessage()); 385 } 386 else { 387 result = Integer.compare(columnNo, other.columnNo); 388 } 389 } 390 return result; 391 } 392 393 /** 394 * <p> 395 * Custom ResourceBundle.Control implementation which allows explicitly read 396 * the properties files as UTF-8. 397 * </p> 398 * 399 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 400 */ 401 public static class Utf8Control extends Control { 402 @Override 403 public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat, 404 ClassLoader aLoader, boolean aReload) throws IOException { 405 // The below is a copy of the default implementation. 406 final String bundleName = toBundleName(aBaseName, aLocale); 407 final String resourceName = toResourceName(bundleName, "properties"); 408 InputStream stream = null; 409 if (aReload) { 410 final URL url = aLoader.getResource(resourceName); 411 if (url != null) { 412 final URLConnection connection = url.openConnection(); 413 if (connection != null) { 414 connection.setUseCaches(false); 415 stream = connection.getInputStream(); 416 } 417 } 418 } 419 else { 420 stream = aLoader.getResourceAsStream(resourceName); 421 } 422 ResourceBundle resourceBundle = null; 423 if (stream != null) { 424 final Reader streamReader = new InputStreamReader(stream, "UTF-8"); 425 try { 426 // Only this line is changed to make it to read properties files as UTF-8. 427 resourceBundle = new PropertyResourceBundle(streamReader); 428 } 429 finally { 430 stream.close(); 431 } 432 } 433 return resourceBundle; 434 } 435 } 436}