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