1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
2 *
3 * This program and the accompanying materials are made available under
4 * the terms of the Common Public License v1.0 which accompanies this distribution,
5 * and is available at http://www.eclipse.org/legal/cpl-v10.html
6 *
7 * $Id: ExceptionCommon.java,v 1.1.1.1.2.2 2004/07/10 03:34:52 vlad_r Exp $
8 */
9package com.vladium.util.exception;
10
11import java.io.PrintStream;
12import java.io.PrintWriter;
13import java.text.MessageFormat;
14import java.util.Collections;
15import java.util.Enumeration;
16import java.util.HashMap;
17import java.util.Locale;
18import java.util.Map;
19import java.util.ResourceBundle;
20
21import com.vladium.util.IJREVersion;
22
23// TODO: embed build # in error codes
24
25// ----------------------------------------------------------------------------
26/**
27 * TODO: javadoc
28 *
29 * Based on code <a href="http://www.fawcette.com/javapro/2002_12/online/exception_vroubtsov_12_16_02/default_pf.asp">published</a>
30 * by me in JavaPro, 2002.<P>
31 *
32 * This non-instantiable class provides static support functions common to
33 * {@link AbstractException} and {@link AbstractRuntimeException}.<P>
34 *
35 * @author Vlad Roubtsov, (C) 2002
36 */
37abstract class ExceptionCommon implements IJREVersion
38{
39    // public: ................................................................
40
41    /**
42     * This method can be called by static initializers of {@link AbstractException}
43     * and {@link AbstractRuntimeException} subclasses in order to add another
44     * resource bundle to the set that is used to look up error codes. This makes
45     * it possible to extend the set of exception error codes across independently
46     * maintained and built projects.<P>
47     *
48     * <BLOCKQUOTE>
49     * Note that this introduces a possibility of error code name clashes. This
50     * is resolved in the following way:
51     * <UL>
52     *     <LI> when <CODE>getMessage(namespace, code)</CODE> is called, 'code'
53     *     is attempted to be looked up in the resource bundle previously keyed
54     *     under 'namespace';
55     *
56     *     <LI> if no such bundle it found or if it does not contain a value for
57     *     key 'code', the same step is repeated for the superclass of 'namespace';
58     *
59     *     <LI> finally, if all of the above steps fail, the root resource bundle
60     *     specified by {@link #ROOT_RESOURCE_BUNDLE_NAME} is searched.
61     * </UL>
62     *
63     * This strategy ensures that error codes follow inheritance and hiding rules
64     * similar to Java static methods.<P>
65     * </BLOCKQUOTE>
66     *
67     * <B>IMPORTANT:</B> this method must be called from static class initializers
68     * <I>only</I>.<P>
69     *
70     * There is no visible state change if the indicated resource is not found
71     * or if it has been added already under the same key.<P>
72     *
73     * @param namespace the Class object acting as the namespace key for the
74     * resource bundle identified by 'messageResourceBundleName'. <I>This should
75     * be the calling class.</I> [the method is a no-op if this is null]
76     *
77     * @param messageResourceBundleName name of a bundle (path relative to 'namespace'
78     * package) to add to the set from which error code mappings are retrieved
79     * [the method is a no-op if this is null or an empty string]
80     *
81     * @return ResourceBundle that corresponds to 'namespace' key or null if
82     * no such bundle could be loaded
83     *
84     * @throws Error if 'namespace' does not correspond to an exception class derived
85     * from {@link AbstractException} or {@link AbstractRuntimeException}.
86     *
87     * @see #lookup
88     */
89    public static ResourceBundle addExceptionResource (final Class namespace,
90                                                       final String messageResourceBundleName)
91    {
92        if ((namespace != null) && (messageResourceBundleName != null)
93            && (messageResourceBundleName.length () > 0))
94        {
95            // bail out if the some other exception hierarchy attempts
96            // to use this functionality:
97            if (! ABSTRACT_EXCEPTION.isAssignableFrom (namespace)
98                && ! ABSTACT_RUNTIME_EXCEPTION.isAssignableFrom (namespace))
99            {
100                throw new Error ("addExceptionResource(): class [" + namespace +
101                    "] is not a subclass of " + ABSTRACT_EXCEPTION.getName () +
102                    " or " + ABSTACT_RUNTIME_EXCEPTION.getName ());
103            }
104
105            // try to load resource bundle
106
107            ResourceBundle temprb = null;
108            String nameInNamespace = null;
109            try
110            {
111                nameInNamespace = getNameInNamespace (namespace, messageResourceBundleName);
112
113                //temprb = ResourceBundle.getBundle (nameInNamespace);
114
115                ClassLoader loader = namespace.getClassLoader ();
116                if (loader == null) loader = ClassLoader.getSystemClassLoader ();
117
118                temprb = ResourceBundle.getBundle (nameInNamespace, Locale.getDefault (), loader);
119            }
120            catch (Throwable ignore)
121            {
122               // ignored intentionally: if the exception codes rb is absent,
123               // we are still in a supported configuration
124               temprb = null;
125            }
126
127            if (temprb != null)
128            {
129                synchronized (s_exceptionCodeMap)
130                {
131                    final ResourceBundle currentrb =
132                        (ResourceBundle) s_exceptionCodeMap.get (namespace);
133                    if (currentrb != null)
134                        return currentrb;
135                    else
136                    {
137                        s_exceptionCodeMap.put (namespace, temprb);
138                        return temprb;
139                    }
140                }
141            }
142        }
143
144        return null;
145    }
146
147    // protected: .............................................................
148
149    // package: ...............................................................
150
151
152    static void printStackTrace (Throwable t, final PrintWriter out)
153    {
154        if (JRE_1_4_PLUS)
155        {
156            if (t instanceof IThrowableWrapper)
157            {
158                final IThrowableWrapper tw = (IThrowableWrapper) t;
159
160                tw.__printStackTrace (out);
161            }
162            else
163            {
164                t.printStackTrace (out);
165            }
166        }
167        else
168        {
169            for (boolean first = true; t != null; )
170            {
171                if (first)
172                    first = false;
173                else
174                {
175                    out.println ();
176                    out.println (NESTED_THROWABLE_HEADER);
177                }
178
179                if (t instanceof IThrowableWrapper)
180                {
181                    final IThrowableWrapper tw = (IThrowableWrapper) t;
182
183                    tw.__printStackTrace (out);
184                    t = tw.getCause ();
185                }
186                else
187                {
188                    t.printStackTrace (out);
189                    break;
190                }
191            }
192        }
193    }
194
195
196    static void printStackTrace (Throwable t, final PrintStream out)
197    {
198        if (JRE_1_4_PLUS)
199        {
200            if (t instanceof IThrowableWrapper)
201            {
202                final IThrowableWrapper tw = (IThrowableWrapper) t;
203
204                tw.__printStackTrace (out);
205            }
206            else
207            {
208                t.printStackTrace (out);
209            }
210        }
211        else
212        {
213            for (boolean first = true; t != null; )
214            {
215                if (first)
216                    first = false;
217                else
218                {
219                    out.println ();
220                    out.println (NESTED_THROWABLE_HEADER);
221                }
222
223                if (t instanceof IThrowableWrapper)
224                {
225                    final IThrowableWrapper tw = (IThrowableWrapper) t;
226
227                    tw.__printStackTrace (out);
228                    t = tw.getCause ();
229                }
230                else
231                {
232                    t.printStackTrace (out);
233                    break;
234                }
235            }
236        }
237    }
238
239
240    /**
241     * Provides support for lookup of exception error codes from {@link AbstractException}
242     * and {@link AbstractRuntimeException} and their subclasses.
243     *
244     * @param namespace the Class object acting as the key to the namespace from
245     * which to retrieve the description for 'code' [can be null, in which case
246     * only the root namespace is used for lookup]
247     *
248     * @param code the message string value that was passed into exception
249     * constructor [can be null, in which case null is returned].
250     *
251     * @return looked-up error message or the error code if it could not be
252     * looked up [null is returned on null 'code' input only].
253     *
254     * This method does not throw.
255     *
256     * @see AbstractException#getMessage
257     * @see AbstractRuntimeException#getMessage
258     */
259    static String getMessage (final Class namespace, final String code)
260    {
261        if (code == null) return null;
262
263        try
264        {
265            if (code.length () > 0)
266            {
267                // look the code up in the resource bundle:
268                final String msg = lookup (namespace, code);
269                if (msg == null)
270                {
271                    // if code lookup failed, return 'code' as is:
272                    return code;
273                }
274                else
275                {
276                    return EMBED_ERROR_CODE ? "[" + code + "] " + msg : msg;
277                }
278           }
279           else
280           {
281               return "";
282           }
283        }
284        catch (Throwable t)
285        {
286            // this method must never fail: default to returning the input
287            // verbatim on all unexpected problems
288            return code;
289        }
290    }
291
292    /**
293     * Provides support for lookup of exception error codes from {@link AbstractException}
294     * and {@link AbstractRuntimeException} and their subclasses.
295     *
296     * @param namespace the Class object acting as the key to the namespace from
297     * which to retrieve the description for 'code' [can be null, in which case
298     * only the root namespace is used for lookup]
299     *
300     * @param code the message string value that was passed into exception
301     * constructor [can be null, in which case null is returned].
302     *
303     * @param arguments java.text.MessageFormat-style parameters to be substituted
304     * into the error message once it is looked up.
305     *
306     * @return looked-up error message or the error code if it could not be
307     * looked up [null is returned on null 'code' input only].
308     *
309     * This method does not throw.
310     *
311     * @see AbstractException#getMessage
312     * @see AbstractRuntimeException#getMessage
313     */
314    static String getMessage (final Class namespace, final String code, final Object [] arguments)
315    {
316        if (code == null) return null;
317        final String pattern = getMessage (namespace, code);
318
319        // assertion: pattern != null
320
321        if ((arguments == null) || (arguments.length == 0))
322        {
323            return pattern;
324        }
325        else
326        {
327            try
328            {
329                return MessageFormat.format (pattern, arguments);
330            }
331            catch (Throwable t)
332            {
333                // this method cannot fail: default to returning the input
334                // verbatim on all unexpected problems:
335
336                final StringBuffer msg = new StringBuffer (code + EOL);
337
338                for (int a = 0; a < arguments.length; a ++)
339                {
340                    msg.append ("\t{" + a + "} = [");
341                    final Object arg = arguments [a];
342                    try
343                    {
344                        msg.append (arg.toString ());
345                    }
346                    catch (Throwable e) // guard against bad toString() overrides
347                    {
348                        if (arg != null)
349                            msg.append (arg.getClass ().getName ());
350                        else
351                            msg.append ("null");
352                    }
353                    msg.append ("]");
354                    msg.append (EOL);
355                }
356
357                return msg.toString ();
358            }
359        }
360    }
361
362    // private: ...............................................................
363
364
365    private ExceptionCommon () {} // prevent subclassing
366
367    /**
368     * Internal property lookup method. It implements the lookup scheme described
369     * in {@link #addExceptionResource}.
370     *
371     * @return property value corresponding to 'propertyName' [null if lookup fails]
372     */
373    private static String lookup (Class namespace, final String propertyName)
374    {
375        if (propertyName == null) return null;
376
377        // note: the following does not guard against exceptions that do not subclass
378        // our base classes [done elsewhere], however it will not crash either
379
380        // check extension bundles:
381        if (namespace != null)
382        {
383            ResourceBundle rb;
384            while (namespace != ABSTRACT_EXCEPTION && namespace != ABSTACT_RUNTIME_EXCEPTION
385                   && namespace != THROWABLE && namespace != null)
386            {
387                synchronized (s_exceptionCodeMap)
388                {
389                    rb = (ResourceBundle) s_exceptionCodeMap.get (namespace);
390                    if (rb == null)
391                    {
392                        // check if there is a default bundle to be loaded for this namespace:
393                        if ((rb = addExceptionResource (namespace, "exceptions")) == null)
394                        {
395                            // add an immutable empty bundle to avoid this check in the future:
396                            s_exceptionCodeMap.put (namespace, EMPTY_RESOURCE_BUNDLE);
397                        }
398                    }
399                }
400
401                if (rb != null)
402                {
403                    String propertyValue = null;
404                    try
405                    {
406                        propertyValue = rb.getString (propertyName);
407                    }
408                    catch (Throwable ignore) {}
409                    if (propertyValue != null) return propertyValue;
410                }
411
412                // walk the inheritance chain for 'namespace':
413                namespace = namespace.getSuperclass ();
414            }
415        }
416
417        // if everything fails, check the root bundle:
418        if (ROOT_RESOURCE_BUNDLE != null)
419        {
420            String propertyValue = null;
421            try
422            {
423                propertyValue = ROOT_RESOURCE_BUNDLE.getString (propertyName);
424            }
425            catch (Throwable ignore) {}
426            if (propertyValue != null) return propertyValue;
427        }
428
429        return null;
430    }
431
432    private static String getNameInNamespace (final Class namespace, final String name)
433    {
434        if (namespace == null) return name;
435
436        final String namespaceName = namespace.getName ();
437        final int lastDot = namespaceName.lastIndexOf ('.');
438
439        if (lastDot <= 0)
440            return name;
441        else
442            return namespaceName.substring (0, lastDot + 1)  + name;
443    }
444
445
446    // changes this to 'false' to eliminate repetition of error codes in
447    // the output of getMessage():
448    private static final boolean EMBED_ERROR_CODE = true;
449
450    // the name of the 'root' message resource bundle, derived as
451    // [this package name + ".exceptions"]:
452    private static final String ROOT_RESOURCE_BUNDLE_NAME;      // set in <clinit>
453
454    // the root resource bundle; always checked if all other lookups fail:
455    private static final ResourceBundle ROOT_RESOURCE_BUNDLE;   // set in <clinit>
456
457    // static cache of all loaded resource bundles, populated via addExceptionResource():
458    private static final Map /* Class -> ResourceBundle */ s_exceptionCodeMap = new HashMap ();
459
460    // misc constants:
461
462    private static final String NESTED_THROWABLE_HEADER = "[NESTED EXCEPTION]:";
463    private static final Class THROWABLE                    = Throwable.class;
464    private static final Class ABSTRACT_EXCEPTION           = AbstractException.class;
465    private static final Class ABSTACT_RUNTIME_EXCEPTION    = AbstractRuntimeException.class;
466    /*private*/ static final Enumeration EMPTY_ENUMERATION  = Collections.enumeration (Collections.EMPTY_SET);
467    private static final ResourceBundle EMPTY_RESOURCE_BUNDLE = new ResourceBundle ()
468    {
469        public Object handleGetObject (final String key)
470        {
471            return null;
472        }
473
474        public Enumeration getKeys ()
475        {
476            return EMPTY_ENUMERATION;
477        }
478    };
479
480    // end-of-line terminator for the current platform:
481    private static final String EOL = System.getProperty ("line.separator", "\n");
482
483
484    static
485    {
486        // set the name of ROOT_RESOURCE_BUNDLE_NAME:
487        ROOT_RESOURCE_BUNDLE_NAME = getNameInNamespace (ExceptionCommon.class, "exceptions");
488
489        // set the root resource bundle:
490        ResourceBundle temprb = null;
491        try
492        {
493            temprb = ResourceBundle.getBundle (ROOT_RESOURCE_BUNDLE_NAME);
494        }
495        catch (Throwable ignore)
496        {
497            // if the exception codes rb is absent, we are still in a supported configuration
498        }
499        ROOT_RESOURCE_BUNDLE = temprb;
500    }
501
502} // end of class
503// ----------------------------------------------------------------------------
504