Jason Dillon wrote:
Yes we should, please submit a patch (someone).

It appears that very few programs put there exception messages in resource bundles. I guess we expect anyone who reads them to be a programmer and be able to read English even if not quite like a native. Also it's easier to edit the messages if they're inline. But I won't let that stop me.


Attached is my stab at an answer. Some notes for discussion:

o If you tried replicating for a number of exceptions, the code duplication would get horrific (and not even generics will save you). I'm quite prepared to put together some XSLT, say, if necessary.
o Current version puts everything (ever) in one resource file. However from previous point, it may need replicating for each project/subproject/package anyway.
o Might want to produce messages in a locale different from machine set one (perhaps even do it a print time rather than throw time).
o Various points on generation of methods lazily and reuse of MessageFormats can be criticised. Is there a better/more appropriate formatter?
o I've put in an option not to fillInStackTrace (if using lazy message formatting), but that's probably asking for trouble. Of course such code can easily be chopped out.


Tom Hawtin
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.Map;

/**
 * Handles creation of exception with lazy i18n messages and option stack 
traces.
 *
 * <p>Typeical use:
 * <pre> } catch (ServiceException exc) {
 *      throw Exceptions.newMyException(exc, "nastyMsgKey");
 * }</pre>
 *
 * <p>Do we neceessarily want to use the machine set locale?
 * <p>This currently assumes one large resource bungle with all messages for 
project (ever).
 * Might be better off with an instance approach.
 */
public class Exceptions {
    /**
     * Zero length array of object (clearly constant).
     */
    private static final Object[] emptyObjectArray = new Object[0];
    /**
     * Indicates where exception messages should be created lazily.
     * Lazily created messages are obviously faster if the message is never 
read.
     * In addition if the message is read in a limited number of threads then 
there can be
     *   a dramatic improvement in memory usage, as MessageFormat are cached.
     */
    private static final boolean lazy = true;
    /**
     * Indicates whether lazily created exceptions
     *   should fill in their stack trace from their constructor.
     * This is a highly expensive operation, but also a very useful one.
     * <strong>It is not recommended that this option is set to false.</strong>
     */
    private static final boolean lazyFillInStackTrace = true;
    /**
     * Resources for error messages.
     */
    private static final java.util.ResourceBundle resourceMsgs =
        java.util.ResourceBundle.getBundle("xyz"); // Change this...
    /**
     * Maps keys on to formatters, thread locally soft reference.
     * <p>Not overly convinced this is the right way to do it.
     *   Might be better off with a different formatting method.
     *   Could create formatter each time and not cache.
     *   Could map straight from key the formatter.
     *   SoftReference should be a good enough.
     */
    private static final ThreadLocal/*<
        SoftReference<Map<String,MessageFormat>>
    >*/ formatMaps = new ThreadLocal();
    /**
     * Message if message key is not present in resource bundle.
     * Parameter 0 is set to the key.
     */        
    private static final String messageKeyUnknown =
        resourceMsgs.getString("messageKeyUnknown");
    /**
     * Message if message in resource bundle is not a <code>String</code>.
     * Parameter 0 is set to the key.
     * Parameter 1 is set to the message<code>.toString()</code>.
     */
    private static final String messageNotString =
        resourceMsgs.getString("messageNotString");
    
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     */
    public static IllegalArgumentException newIllegalArgument(
         final String key
    ) {
        return newIllegalArgument(null, key, null);
    }
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param cause Exception that caused this exception.
     *   <code>null</code> indicates this exception was not caused by another.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     */
    public static IllegalArgumentException newIllegalArgument(
         Throwable cause, final String key
    ) {
        return newIllegalArgument(cause, key, null);
    }
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     * @param param0 First parameter for error message.
     */
    public static IllegalArgumentException newIllegalArgument(
         final String key, Object param
    ) {
        return newIllegalArgument(null, key, new Object[] { param });
    }
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param cause Exception that caused this exception.
     *   <code>null</code> indicates this exception was not caused by another.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     * @param param0 First parameter for error message.
     */
    public static IllegalArgumentException newIllegalArgument(
         Throwable cause, final String key, Object param
    ) {
        return newIllegalArgument(cause, key, new Object[] { param });
    }
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     * @param params Parameters for error message.
     *   <code>null</code> indicates no parameters.
     */
    public static IllegalArgumentException newIllegalArgument(
         final String key, Object[] params
    ) {
        return newIllegalArgument(null, key, params);
    }
    /**
     * Create a new exception.
     * Note order different from Exception/Throwable constructors
     *  to disambiguate cause from params.
     * @param cause Exception that caused this exception.
     *   <code>null</code> indicates this exception was not caused by another.
     * @param key Key for error message.
     *   <code>null</code> indicates no message.
     * @param params Parameters for error message.
     *   <code>null</code> indicates no parameters.
     */
    public static IllegalArgumentException newIllegalArgument(
         Throwable cause, final String key, Object[] params
    ) {
        IllegalArgumentException exception;
        if (lazy) {
            final Object[] paramsCopy = (params==null || params.length==0) ?
                emptyObjectArray : (Object[])params.clone();
            class IllegalArgumentException extends 
java.lang.IllegalArgumentException {
                private boolean doFillInStackTrace = lazyFillInStackTrace;
                private String message;
                public IllegalArgumentException() {
                    super();
                    // Further calls to fillInStackTrace() should work.
                    doFillInStackTrace = false;
                }
                public synchronized String getMessage() {
                    // Note, synchronized not strictly necessary for JRE 1.5 or 
later.
                    if (message == null) {
                        message = formatKey(key, paramsCopy);
                    }
                    return message;
                }
                public Throwable fillInStackTrace() {
                    if (doFillInStackTrace) {
                        return super.fillInStackTrace();
                    } else {
                        return this;
                    }
                }
            }
            exception = new IllegalArgumentException();
        } else {
            String message = formatKey(key, params);
            exception = new IllegalArgumentException(message);
        }
        exception.initCause(cause);
        return exception;
    }
    /**
     * Format message from key.
     * @param key Key for message in resources.
     *   <code>null</code> indicates no message.
     * @param params Parameters for message.
     *   <code>null</code> indicates no parameters.
     * @return Formatted message.
     *   <code>null</code> iff <code>key<code> is <code>null</code>.     
     */
    private static String formatKey(String key, Object[] params) {
        if (key == null) {
            return null;
        }
        try {
            try {
                String message = resourceMsgs.getString(key);
                return formatMessage(message, params);
            } catch (java.lang.ClassCastException exc) {
                Object message = resourceMsgs.getObject(key);
                return formatMessage(messageNotString, new Object[] {
                        key, message==null ? null : message.toString()
                });
            }
        } catch (java.util.MissingResourceException exc) {
            return formatMessage(messageKeyUnknown, new Object[] {
                    key
            });
        }
        
    }
    /**
     * Format message.
     * @param message In <code>java.text.MessageFormat</code> format.
     *   <code>null</code> indicates no message.
     * @param params Parameters for message.
     *   <code>null</code> indicates no parameters.
     * @return Formatted message.
     */
    private static String formatMessage(String message, Object[] params) {
        if (message == null) {
            return message;
        }
        
        // Short cut if no parameter or quote substitution required.
        if (message.indexOf('{') == -1 && message.indexOf('\'') == -1) {
             return message;
        }
        
        // Get, or create, map of keys to formatters.
        SoftReference/*<Map<String,MessageFormat>>*/ ref = 
(SoftReference)formatMaps.get();
        Map/*<String,MessageFormat>*/ formatMap =
            ref==null ? null : (Map)ref.get();
        if (formatMap == null) {
            formatMap = new java.util.HashMap();
            formatMaps.set(new SoftReference(formatMap));
        }
        
        // Get, or create, format map.
        MessageFormat format = (MessageFormat)formatMap.get(message);
        if (format == null) {
            format = new MessageFormat(message);
            formatMap.put(message, format);
        }
        
        return format.format(message, params==null ? emptyObjectArray : 
params).toString();
    }
}

Reply via email to