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}