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