001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 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//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle.api; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.Reader; 025import java.io.Serializable; 026import java.net.URL; 027import java.net.URLConnection; 028import java.text.MessageFormat; 029import java.util.Arrays; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.Locale; 033import java.util.Map; 034import java.util.MissingResourceException; 035import java.util.PropertyResourceBundle; 036import java.util.ResourceBundle; 037import java.util.ResourceBundle.Control; 038 039 040/** 041 * Represents a message that can be localised. The translations come from 042 * message.properties files. The underlying implementation uses 043 * java.text.MessageFormat. 044 * 045 * @author Oliver Burn 046 * @author lkuehne 047 * @version 1.0 048 */ 049public final class LocalizedMessage 050 implements Comparable<LocalizedMessage>, Serializable 051{ 052 /** Required for serialization. */ 053 private static final long serialVersionUID = 5675176836184862150L; 054 055 /** hash function multiplicand */ 056 private static final int HASH_MULT = 29; 057 058 /** the locale to localise messages to **/ 059 private static Locale sLocale = Locale.getDefault(); 060 061 /** 062 * A cache that maps bundle names to RessourceBundles. 063 * Avoids repetitive calls to ResourceBundle.getBundle(). 064 */ 065 private static final Map<String, ResourceBundle> BUNDLE_CACHE = 066 Collections.synchronizedMap(new HashMap<String, ResourceBundle>()); 067 068 /** the line number **/ 069 private final int lineNo; 070 /** the column number **/ 071 private final int colNo; 072 073 /** the severity level **/ 074 private final SeverityLevel severityLevel; 075 076 /** the id of the module generating the message. */ 077 private final String moduleId; 078 079 /** the default severity level if one is not specified */ 080 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR; 081 082 /** key for the message format **/ 083 private final String key; 084 085 /** arguments for MessageFormat **/ 086 private final Object[] args; 087 088 /** name of the resource bundle to get messages from **/ 089 private final String bundle; 090 091 /** class of the source for this LocalizedMessage */ 092 private final Class<?> sourceClass; 093 094 /** a custom message overriding the default message from the bundle. */ 095 private final String customMessage; 096 097 @Override 098 public boolean equals(Object object) 099 { 100 if (this == object) { 101 return true; 102 } 103 if (!(object instanceof LocalizedMessage)) { 104 return false; 105 } 106 107 final LocalizedMessage localizedMessage = (LocalizedMessage) object; 108 109 if (colNo != localizedMessage.colNo) { 110 return false; 111 } 112 if (lineNo != localizedMessage.lineNo) { 113 return false; 114 } 115 if (!key.equals(localizedMessage.key)) { 116 return false; 117 } 118 119 if (!Arrays.equals(args, localizedMessage.args)) { 120 return false; 121 } 122 // ignoring bundle for perf reasons. 123 124 // we currently never load the same error from different bundles. 125 126 return true; 127 } 128 129 @Override 130 public int hashCode() 131 { 132 int result; 133 result = lineNo; 134 result = HASH_MULT * result + colNo; 135 result = HASH_MULT * result + key.hashCode(); 136 for (final Object element : args) { 137 result = HASH_MULT * result + element.hashCode(); 138 } 139 return result; 140 } 141 142 /** 143 * Creates a new <code>LocalizedMessage</code> instance. 144 * 145 * @param lineNo line number associated with the message 146 * @param colNo column number associated with the message 147 * @param bundle resource bundle name 148 * @param key the key to locate the translation 149 * @param args arguments for the translation 150 * @param severityLevel severity level for the message 151 * @param moduleId the id of the module the message is associated with 152 * @param sourceClass the Class that is the source of the message 153 * @param customMessage optional custom message overriding the default 154 */ 155 public LocalizedMessage(int lineNo, 156 int colNo, 157 String bundle, 158 String key, 159 Object[] args, 160 SeverityLevel severityLevel, 161 String moduleId, 162 Class<?> sourceClass, 163 String customMessage) 164 { 165 this.lineNo = lineNo; 166 this.colNo = colNo; 167 this.key = key; 168 this.args = (null == args) ? null : args.clone(); 169 this.bundle = bundle; 170 this.severityLevel = severityLevel; 171 this.moduleId = moduleId; 172 this.sourceClass = sourceClass; 173 this.customMessage = customMessage; 174 } 175 176 /** 177 * Creates a new <code>LocalizedMessage</code> instance. 178 * 179 * @param lineNo line number associated with the message 180 * @param colNo 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 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 */ 188 public LocalizedMessage(int lineNo, 189 int colNo, 190 String bundle, 191 String key, 192 Object[] args, 193 String moduleId, 194 Class<?> sourceClass, 195 String customMessage) 196 { 197 this(lineNo, 198 colNo, 199 bundle, 200 key, 201 args, 202 DEFAULT_SEVERITY, 203 moduleId, 204 sourceClass, 205 customMessage); 206 } 207 208 /** 209 * Creates a new <code>LocalizedMessage</code> instance. 210 * 211 * @param lineNo line number associated with the message 212 * @param bundle resource bundle name 213 * @param key the key to locate the translation 214 * @param args arguments for the translation 215 * @param severityLevel severity level for the message 216 * @param moduleId the id of the module the message is associated with 217 * @param sourceClass the source class for the message 218 * @param customMessage optional custom message overriding the default 219 */ 220 public LocalizedMessage(int lineNo, 221 String bundle, 222 String key, 223 Object[] args, 224 SeverityLevel severityLevel, 225 String moduleId, 226 Class<?> sourceClass, 227 String customMessage) 228 { 229 this(lineNo, 0, bundle, key, args, severityLevel, moduleId, 230 sourceClass, customMessage); 231 } 232 233 /** 234 * Creates a new <code>LocalizedMessage</code> instance. The column number 235 * defaults to 0. 236 * 237 * @param lineNo line number associated with the message 238 * @param bundle name of a resource bundle that contains error messages 239 * @param key the key to locate the translation 240 * @param args arguments for the translation 241 * @param moduleId the id of the module the message is associated with 242 * @param sourceClass the name of the source for the message 243 * @param customMessage optional custom message overriding the default 244 */ 245 public LocalizedMessage( 246 int lineNo, 247 String bundle, 248 String key, 249 Object[] args, 250 String moduleId, 251 Class<?> sourceClass, 252 String customMessage) 253 { 254 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId, 255 sourceClass, customMessage); 256 } 257 258 /** Clears the cache. */ 259 public static void clearCache() 260 { 261 synchronized (BUNDLE_CACHE) { 262 BUNDLE_CACHE.clear(); 263 } 264 } 265 266 /** @return the translated message **/ 267 public String getMessage() 268 { 269 270 final String customMessage = getCustomMessage(); 271 if (customMessage != null) { 272 return customMessage; 273 } 274 275 try { 276 // Important to use the default class loader, and not the one in 277 // the GlobalProperties object. This is because the class loader in 278 // the GlobalProperties is specified by the user for resolving 279 // custom classes. 280 final ResourceBundle bundle = getBundle(this.bundle); 281 final String pattern = bundle.getString(key); 282 return MessageFormat.format(pattern, args); 283 } 284 catch (final MissingResourceException ex) { 285 // If the Check author didn't provide i18n resource bundles 286 // and logs error messages directly, this will return 287 // the author's original message 288 return MessageFormat.format(key, args); 289 } 290 } 291 292 /** 293 * Returns the formatted custom message if one is configured. 294 * @return the formatted custom message or <code>null</code> 295 * if there is no custom message 296 */ 297 private String getCustomMessage() 298 { 299 300 if (customMessage == null) { 301 return null; 302 } 303 304 return MessageFormat.format(customMessage, args); 305 } 306 307 /** 308 * Find a ResourceBundle for a given bundle name. Uses the classloader 309 * of the class emitting this message, to be sure to get the correct 310 * bundle. 311 * @param bundleName the bundle name 312 * @return a ResourceBundle 313 */ 314 private ResourceBundle getBundle(String bundleName) 315 { 316 synchronized (BUNDLE_CACHE) { 317 ResourceBundle bundle = BUNDLE_CACHE 318 .get(bundleName); 319 if (bundle == null) { 320 bundle = ResourceBundle.getBundle(bundleName, sLocale, 321 sourceClass.getClassLoader(), new UTF8Control()); 322 BUNDLE_CACHE.put(bundleName, bundle); 323 } 324 return bundle; 325 } 326 } 327 328 /** @return the line number **/ 329 public int getLineNo() 330 { 331 return lineNo; 332 } 333 334 /** @return the column number **/ 335 public int getColumnNo() 336 { 337 return colNo; 338 } 339 340 /** @return the severity level **/ 341 public SeverityLevel getSeverityLevel() 342 { 343 return severityLevel; 344 } 345 346 /** @return the module identifier. */ 347 public String getModuleId() 348 { 349 return moduleId; 350 } 351 352 /** 353 * Returns the message key to locate the translation, can also be used 354 * in IDE plugins to map error messages to corrective actions. 355 * 356 * @return the message key 357 */ 358 public String getKey() 359 { 360 return key; 361 } 362 363 /** @return the name of the source for this LocalizedMessage */ 364 public String getSourceName() 365 { 366 return sourceClass.getName(); 367 } 368 369 /** @param locale the locale to use for localization **/ 370 public static void setLocale(Locale locale) 371 { 372 sLocale = locale; 373 } 374 375 //////////////////////////////////////////////////////////////////////////// 376 // Interface Comparable methods 377 //////////////////////////////////////////////////////////////////////////// 378 379 /** {@inheritDoc} */ 380 @Override 381 public int compareTo(LocalizedMessage other) 382 { 383 if (getLineNo() == other.getLineNo()) { 384 if (getColumnNo() == other.getColumnNo()) { 385 return getMessage().compareTo(other.getMessage()); 386 } 387 return (getColumnNo() < other.getColumnNo()) ? -1 : 1; 388 } 389 390 return (getLineNo() < other.getLineNo()) ? -1 : 1; 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 private static class UTF8Control extends Control 402 { 403 @Override 404 public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat, 405 ClassLoader aLoader, boolean aReload) throws IllegalAccessException, 406 InstantiationException, IOException 407 { 408 // The below is a copy of the default implementation. 409 final String bundleName = toBundleName(aBaseName, aLocale); 410 final String resourceName = toResourceName(bundleName, "properties"); 411 ResourceBundle bundle = null; 412 InputStream stream = null; 413 if (aReload) { 414 final URL url = aLoader.getResource(resourceName); 415 if (url != null) { 416 final URLConnection connection = url.openConnection(); 417 if (connection != null) { 418 connection.setUseCaches(false); 419 stream = connection.getInputStream(); 420 } 421 } 422 } 423 else { 424 stream = aLoader.getResourceAsStream(resourceName); 425 } 426 if (stream != null) { 427 try (Reader streamReader = new InputStreamReader(stream, "UTF-8")) { 428 // Only this line is changed to make it to read properties files as UTF-8. 429 bundle = new PropertyResourceBundle(streamReader); 430 } finally { 431 stream.close(); 432 } 433 } 434 return bundle; 435 } 436 } 437}