I think the only way of achieving this would be to have a formatExceptionInLocale(Locale) method which could choose the translation at print time, rather than at construction time.
I guess getLocalizedMessage() should print a message localized for the environment it is called on, rather than the environment the exception was originally created in. The exception may o fcourse have been serialized. Then there's the question as to whether the message should be looked up on the constructing and printing machine.
Anyway I've updated my code to, amongst other things, use XSLT (Exception.xsl should be applied to exception-list.xml with Exceptions.xml being picked up by document()). It should be easy enough to create as many versions of the code as you want. Perhaps one per package. Perhaps coarser. May want to split out exception subclasses so that they are reused between different "Exceptions" factory classes.
Pros:
o Significantly simplifies exception (re)throwing code.
o More obvious when throwing code is doing something unusual.
o I18n, chaining and logging all under one roof.
Cons:
o Adds to complexity of build process.
o The XSLT is pretty icky (although you don't need to so much as look at that to change the supported exceptions or the code template).
o Who wants i18n of exception messages anyway?
Tom Hawtin
<!DOCTYPE code SYSTEM "exceptions-code.dtd"> <code>package <gen-package/>; <![CDATA[ import java.lang.ref.SoftReference; import java.text.MessageFormat; import java.util.Locale; import java.util.Map; import org.apache.geronimo.utils.LocalizedMessage;
/**
* Handles creation of exception with lazy i18n messages and option stack traces.
*
* This class supports the following exception types:<ul>
]]><exceptions><![CDATA[ * <li><code>]]><link/><![CDATA[</code></li>
]]></exceptions><![CDATA[ * </ul>
* <p>Typical use:
* <pre> } catch (ServiceException exc) {
* throw Exceptions.newMyException(exc, "nastyMsgKey");
* }</pre>
*
* <p>This currently assumes one large resource bungle
* with all messages for project (ever).
* Might be better off with an instance approach.
*/
]]>public class <gen-class/> {<![CDATA[
/**
* 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 <code>MessageFormat</code> instances are cached.
*/
private static final boolean lazy = true;
/**
* Indicates whether lazily created exceptions
* should skip the stack trace fill in from their super constructor.
* This is a highly expensive operation, but also a very useful one.
* <strong>It is not recommended that this option is set to true.</strong>
*/
private static final boolean lazySkipFillInStackTrace = false;
/**
* Resources for error messages.
*/
private static final java.util.ResourceBundle resourceMsgs =
java.util.ResourceBundle.getBundle(]]>"<resource-bundle/>"<![CDATA[);
/**
* 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<Locale,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");
]]><exceptions><!--
Perhaps want to add some factory methods
with single param typed as String, Number, Date, etc.
--><has-convenience-factory-methods><![CDATA[
/**
* Create a new exception.
* @param key Key for error message.
* <code>null</code> indicates no message.
* If first character is a colon, the rest of the key is taken as the message.
*/
]]>public static <class/> <factory-method/>(
final String key
) {
return <factory-method/>(key, (Object[])null, (<cause/>)null);
}<![CDATA[
/**
* Create a new exception.
* @param key Key for error message.
* <code>null</code> indicates no message.
* If first character is a colon, the rest of the key is taken as the message.
* @param cause Exception that caused this exception.
* <code>null</code> indicates this exception was not caused by another.
*/
]]>public static <class/> <factory-method/>(
final String key, <cause/> cause
) {
return <factory-method/>(key, (Object[])null, cause);
}<![CDATA[
/**
* Create a new exception.
* @param key Key for error message.
* <code>null</code> indicates no message.
* If first character is a colon, the rest of the key is taken as the message.
* @param params Parameters for error message.
* <code>null</code> indicates no parameters.
*/
]]>public static <class/> <factory-method/>(
final String key, Object[] params
) {
return <factory-method/>(key, params, (<cause/>)null);
}</has-convenience-factory-methods><constructors><![CDATA[
/**
* Create a new exception.x
* @param key Key for error message.
* <code>null</code> indicates no message.
* If first character is a colon, the rest of the key is taken as the message.
* @param params Parameters for error message.
* <code>null</code> indicates no parameters.
* @param cause Exception that caused this exception.
* <code>null</code> indicates this exception was not caused by another.
]]><extra-params-docs/> */
public static <class/> <factory-method/>(
String key, Object[] params, <cause/> cause<extra-params-formal/>
) {
if (lazy) {
return new <nested-class/>(key, params, cause<extra-params-actual/>);
} else {
String message = formatKey(Locale.US, key, params);
<class/> exception = new
<class/>(
message<if-cause>, cause</if-cause><extra-params-actual/>
);
<if-not-cause>exception.initCause(cause);
</if-not-cause>return exception;
}
}</constructors><![CDATA[
/**
* ]]><class/><![CDATA[ with lazy localizing message
* and optional fillInStackTrace skip.
* <p>Perhaps should look up no-localised message
* immediately/on serialization?
*/
]]>private static class <nested-class/> extends <class/>
implements LocalizedMessage
{<![CDATA[
private final String key;
private final Object[] params;
private transient boolean skipFillInStackTrace =
lazySkipFillInStackTrace;
/**
* Lazy evaluated message, for locale in [EMAIL PROTECTED] #messageLocale}.
*/
private transient String message;
/**
* Locale of message cached in [EMAIL PROTECTED] #message}.
*/
private transient Locale messageLocale;
]]><constructors>
public <nested-class/>(
String key,
Object[] params,
<cause/> cause<extra-params-formal/>
) {
<if-cause>// We supply a null message as well,
// for the benefit of java.rmi.RemoteException.
super((String)null, cause<extra-params-actual/>);</if-cause>
<if-not-cause>// We supply a null message as well,
// for simplicity.
super((String)null<extra-params-actual/>);</if-not-cause>
this.key = key;
// Do we want to clone the elements as well here?
// For instance Date, or indeed perhaps anything not in java.lang?
this.params = (params==null || params.length==0) ?
emptyObjectArray : (Object[])params.clone();
// Further calls to fillInStackTrace() should work.
this.skipFillInStackTrace = false;
<if-not-cause>
initCause(cause);
</if-not-cause>
}</constructors><![CDATA[
public String getMessage() {
return getLocalizedMessage(null);
}
public String getLocalizedMessage() {
return getLocalizedMessage(Locale.getDefault());
}
/**
* Returns message for locale.
*/
public String getLocalizedMessage(Locale locale) {
// Check cached value.
synchronized (this) {
if (message != null || locale == messageLocale) {
return message;
}
}
// Format message.
String message = formatKey(locale, key, params);
// Cache result value.
synchronized (this) {
this.message = message;
messageLocale = locale;
}
return message;
}
public Throwable fillInStackTrace() {
if (skipFillInStackTrace) {
return this;
} else {
return super.fillInStackTrace();
}
}
}
]]></exceptions><![CDATA[
/**
* Format message from key.
* @param locale Locale to format message into.
* @param key Key for message in resources.
* <code>null</code> indicates no message.
* If first character is a colon, the rest of the key is taken as the 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(
Locale locale, String key, Object[] params
) {
if (key == null) {
return null;
}
if (key.length() >= 1 && key.charAt(0) == ':') {
return formatMessage(locale, key.substring(1), params);
}
try {
try {
String message = resourceMsgs.getString(key);
return formatMessage(locale, message, params);
} catch (java.lang.ClassCastException exc) {
Object message = resourceMsgs.getObject(key);
return formatMessage(locale, messageNotString, new Object[] {
key, message==null ? null : message.toString()
});
}
} catch (java.util.MissingResourceException exc) {
return formatMessage(locale, messageKeyUnknown, new Object[] {
key
});
}
}
/**
* Format message.
* @param locale Locale to format message into.
* @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(
Locale locale, 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 locales to map of keys to formatters.
SoftReference/*<Map<Locale,Map<String,MessageFormat>>*/ ref =
(SoftReference)formatMaps.get();
Map/*<Locale,Map<String,MessageFormat>>*/ formatLocaleMap =
ref==null ? null : (Map)ref.get();
if (formatLocaleMap == null) {
formatLocaleMap = new java.util.HashMap();
formatMaps.set(new SoftReference(formatLocaleMap));
}
// Get, or create, locale's map of keys to formatters.
Map/*<Map<String,MessageFormat>>*/ formatMap =
ref==null ? null : (Map)formatLocaleMap.get(locale);
if (formatMap == null) {
formatMap = new java.util.HashMap();
formatLocaleMap.put(locale, formatMap);
}
// Get, or create, format map.
MessageFormat format = (MessageFormat)formatMap.get(message);
if (format == null) {
format = new MessageFormat(message, locale);
formatMap.put(message, format);
}
return format.format(
message, params==null ? emptyObjectArray : params
).toString();
}
}
]]></code>
<!-- Root element for Java file. -->
<!ELEMENT code (#PCDATA | gen-package | gen-class | resource-bundle)* >
<!-- Replaced by name of generated class' package. -->
<!ELEMENT gen-package EMPTY>
<!-- Replaced by unqualified name of generated class. -->
<!ELEMENT gen-class EMPTY>
<!-- Replaced by name of resource bundle (not quoted). -->
<!ELEMENT resource-bundle EMPTY>
<!-- Elements the may appear within context of a exception. -->
<!-- No idea why this doesn't work... -->
<!ENTITY % exception-content "(link | class | factory-method | nested-class |
cause | if-cause | if-not-cause)*">
<!-- Body repeated for each available exception. -->
<!--ELEMENT exceptions %exception-content; -->
<!ELEMENT exceptions (#PCDATA | link | class | factory-method | nested-class
| cause | if-cause | if-not-cause | constructors |
has-convenience-factory-methods)* >
<!-- Repalce by a JavaDoc link to the exception class. -->
<!ELEMENT link EMPTY>
<!-- Repalce by the name of the exception class. -->
<!ELEMENT class EMPTY>
<!-- Repalce by the name of the factory method to generate the exception. -->
<!ELEMENT factory-method EMPTY>
<!-- Repalce by the name of the nested class that should extend the
exception. -->
<!ELEMENT nested-class EMPTY>
<!--
Type of root cause exception.
Typically java.lang.Throwable, but sometimes java.lang.Exception.
-->
<!ELEMENT cause EMPTY>
<!-- Indicates body only applicable if cause is specified for the exception.
-->
<!ELEMENT if-cause (#PCDATA | link | class | factory-method | nested-class |
cause | if-cause | if-not-cause | constructors | extra-params-docs |
extra-params-formal | extra-params-actual)* >
<!-- Indicates body only applicable if cause is not specified for the
exception. -->
<!ELEMENT if-not-cause (#PCDATA | link | class | factory-method |
nested-class | cause | if-cause | if-not-cause | constructors |
extra-params-docs | extra-params-formal | extra-params-actual)* >
<!-- Body repeated for each available constructor for exception. -->
<!ELEMENT constructors (#PCDATA | link | class | factory-method |
nested-class | cause | if-cause | if-not-cause | constructors |
extra-params-docs | extra-params-formal | extra-params-actual)* >
<!-- Replaced by JavaDocs for the constructor's extra parameters. -->
<!ELEMENT extra-params-docs EMPTY>
<!-- Replaced by a formal parameter list for the constructor's extra
parameters. -->
<!ELEMENT extra-params-formal EMPTY>
<!-- Replaced by a actual argument list for the constructor's extra
parameters. -->
<!ELEMENT extra-params-actual EMPTY>
<!-- Indicates body only applicable if a constructor with no extra
parameterss. -->
<!ELEMENT has-convenience-factory-methods (#PCDATA | link | class |
factory-method | nested-class | cause | if-cause | if-not-cause | constructors
| extra-params-docs | extra-params-formal | extra-params-actual)* >
<!DOCTYPE exception-list SYSTEM "exception-list.dtd">
<exception-list
class="org.apache.geronimo.utils.Exceptions"
resource-bundle="exceptions"
>
<exception class="java.lang.IllegalArgumentException"/>
<exception class="java.lang.IllegalStateException"/>
<exception class="java.lang.NullPointerException"/>
<exception class="java.lang.RuntimeException"/>
<exception class="java.lang.SecurityException"/>
<exception class="java.lang.UnsupportedOperationException"/>
<exception class="java.io.IOException"/>
<exception class="java.io.FileNotFoundException"/>
<exception class="java.net.UnknownServiceException"/>
<exception
class="java.rmi.RemoteException"
cause="java.lang.Throwable"
/>
<!--
<exception
class="javax.ejb.EJBException"
cause="java.lang.Exception"
/>
<exception class="javax.ejb.TransactionRequiredLocalException"/>
<exception class="javax.enterprise.deploy.spi.exceptions.ConfigurationException"/>
<exception class="javax.enterprise.deploy.spi.exceptions.DeploymentManagerCreationException"/>
<exception class="javax.enterprise.deploy.spi.exceptions.DConfigBeanVersionUnsupportedException"/>
<exception
class="javax.mail.MessagingException"
cause="java.lang.Exception"
/>
<exception class="javax.mail.MethodNotSupportedException"/>
<exception class="javax.mail.internet.ParseException"/>
<exception class="javax.resource.spi.work.NotSupportedException"/>
<exception class="javax.resource.ResourceException"/>
<!- -exception class="javax.transaction.NotSupportedException"/- ->
<exception class="javax.transaction.SystemException"/>
<exception class="javax.transaction.TransactionRequiredException"/>
<exception class="org.apache.geronimo.common.NullArgumentException"/>
<exception class="org.apache.geronimo.common.StartException"/>
<exception class="org.apache.geronimo.common.StopException"/>
<exception class="org.apache.geronimo.common.UnreachableStatementException"/>
<exception class="org.apache.geronimo.deploy.DeploymentException"/>
<exception class="org.apache.geronimo.ejb.context.EJBTransactionException"/>
<exception class="org.apache.geronimo.transaction.GeronimoTransactionRolledbackException"/>
<exception class="org.apache.geronimo.transaction.GeronimoTransactionRolledbackLocalException"/>-->
<!--
<exception class="org.apache.geronimo.twiddle.command.CommandException"/>
<exception class="org.apache.geronimo.twiddle.command.CommandNotFoundException"/>
-->
</exception-list>
<!ELEMENT exception-list (exception*) >
<!-- Fully qualified name of class to generate. -->
<!ATTLIST exception-list class CDATA #REQUIRED>
<!-- Name of resources (in same package as class). -->
<!ATTLIST exception-list resource-bundle CDATA #REQUIRED>
<!-- Details of an exception to handle. -->
<!ELEMENT exception (constructor)*>
<!-- Fully qualified class name of exception. -->
<!ATTLIST exception class CDATA #REQUIRED>
<!--
If present, fully qualified class name of cause exception.
Cause should be pased to the new exceptions constructor after the message.
If not present, initCause is called with Throwable.
-->
<!ATTLIST exception cause CDATA #IMPLIED>
<!--
Details of a constructor.
Extra arguments are assumed to follow on from message and optionally root
cause.
If none present assumes the is a no-arg constructor,
or one that takes message and cause if cause attrivute present in
exception element.
-->
<!ELEMENT constructor (param)*>
<!-- Details of a parameter. -->
<!ELEMENT param (java-doc)?>
<!-- Type of parameter (e.g. "int") and name (e.g. "index"). -->
<!ATTLIST param
type CDATA #REQUIRED
name CDATA #REQUIRED
>
<!--
JavaDodc for parameter.
Remember to escape special characters.
-->
<!ELEMENT java-doc (#PCDATA)>
<!-- Weave Java XML with exception list. Apply to exceptions list XML document. Java XML document loaded through document function, and drives the generation. As generating a text document, no need for the xsl namespace prefix. --> <stylesheet xmlns="http://www.w3.org/1999/XSL/Transform" version="1.0"> <output method="text" omit-xml-declaration="yes" media-type="text/plain"/> <variable name="exception-list" select="exception-list"/> <!-- Actually we want the Java XML document to drive. --> <template match="/exception-list"> <apply-templates select="document('Exceptions.xml')/code"/> </template> <template match="gen-package"> <call-template name="package-name"> <with-param name="qualified-class-name" select="$exception-list/@class"/> </call-template> </template> <template match="resource-bundle"> <call-template name="package-name"> <with-param name="qualified-class-name" select="$exception-list/@class"/> </call-template> <text>.</text> <value-of select="$exception-list/@resource-bundle"/> </template> <!-- Output package name part of a qualified class name. e.g. "xyz.uvw.MyClass" gives "xyz.uvw". Somewhat complicated due to XSLT (1.0)'s poor string handling. --> <template name="package-name"> <!-- Class name, maybe qualified by (part of) package name. --> <param name="qualified-class-name"/> <choose> <when test="contains($qualified-class-name, '.')"> <!-- Class name has dot, output package name part and recurse with rest of string. --> <variable name="right" select="substring-after($qualified-class-name, '.')" /> <!-- Package name, plus dot if not last. --> <value-of select="substring-before($qualified-class-name, '.')"/> <if test="contains($right, '.')">.</if> <call-template name="package-name"> <with-param name="qualified-class-name" select="$right"/> </call-template> </when> <otherwise> <!-- No qualification - ignore class name. --> </otherwise> </choose> </template> <template match="gen-class"> <call-template name="class-name"> <with-param name="qualified-class-name" select="$exception-list/@class"/> </call-template> </template> <!-- Output unqualified part of a package qualified class name. e.g. "xyz.uvw.MyClass" gives "MyClass". Somewhat complicated due to XSLT (1.0)'s poor string handling. --> <template name="class-name"> <!-- Class name, maybe qualified by (part of) package name. --> <param name="qualified-class-name"/> <choose> <when test="contains($qualified-class-name, '.')"> <!-- Class name has dot, recurse with rest of string. --> <variable name="right" select="substring-after($qualified-class-name, '.')" /> <call-template name="class-name"> <with-param name="qualified-class-name" select="$right"/> </call-template> </when> <otherwise> <!-- No qualification - output it. --> <value-of select="$qualified-class-name"/> </otherwise> </choose> </template> <!-- Execute body for each exception listed. --> <template match="exceptions"> <variable name="body" select="*|node()"/> <for-each select="$exception-list/exception"> <apply-templates select="$body"> <with-param name="exc" select="."/> </apply-templates> <!--value-of select="."/> <value-of select="$body"/--> </for-each> </template> <!-- Defined if there exists a constructor defined with no extra parameter (or no constructors specified at all). --> <template match="has-convenience-factory-methods"> <param name="exc"/> <if test="$exc[constructor[not(param)]] or not($exc[constructor])"> <apply-templates select="*|node()"> <with-param name="exc" select="$exc"/> </apply-templates> </if> </template> <!-- Execute body for each constructor listed. --> <template match="constructors"> <param name="exc"/> <variable name="body" select="*|node()"/> <choose> <when test="$exc/constructor"> <for-each select="$exc/constructor"> <apply-templates select="$body"> <with-param name="exc" select="$exc"/> <with-param name="ctor" select="."/> </apply-templates> </for-each> </when> <otherwise> <!-- No constructors specified - assume default. --> <apply-templates select="$body"> <with-param name="exc" select="$exc"/> </apply-templates> </otherwise> </choose> </template> <template match="extra-params-docs"> <param name="exc"/> <param name="ctor"/> <if test="$ctor"> <for-each select="$ctor/param"> <value-of select="concat(' * @param ', @name, ' ', java-doc, '
')"/> </for-each> </if> </template> <template match="extra-params-formal"> <param name="exc"/> <param name="ctor"/> <if test="$ctor"> <for-each select="$ctor/param"> <value-of select="concat(', ', @type, ' ', @name)"/> </for-each> </if> </template> <template match="extra-params-actual"> <param name="exc"/> <param name="ctor"/> <if test="$ctor"> <for-each select="$ctor/param"> <value-of select="concat(', ', @name)"/> </for-each> </if> </template> <template match="if-cause"> <param name="exc"/> <param name="ctor"/> <if test="$exc/@cause"> <apply-templates select="*|node()"> <with-param name="exc" select="$exc"/> <with-param name="ctor" select="$ctor"/> </apply-templates> </if> </template> <template match="if-not-cause"> <param name="exc"/> <param name="ctor"/> <if test="not($exc/@cause)"> <apply-templates select="*|node()"> <with-param name="exc" select="$exc"/> <with-param name="ctor" select="$ctor"/> </apply-templates> </if> </template> <template match="cause"> <param name="exc"/> <choose> <when test="$exc/@cause"> <value-of select="$exc/@cause"/> </when> <otherwise>java.lang.Throwable</otherwise> </choose> </template> <template match="class"> <param name="exc"/> <value-of select="$exc/@class"/> </template> <template match="link"> <param name="exc"/> <value-of select="concat('[EMAIL PROTECTED] ', $exc/@class, '}')"/> </template> <template match="nested-class"> <param name="exc"/> <!-- Check valid --> <text>I18n</text> <call-template name="class-name"> <with-param name="qualified-class-name" select="$exc/@class"/> </call-template> </template> <template match="factory-method"> <param name="exc"/> <!-- Prefix with "new" --> <text>new</text> <!-- Ignore postfix of "Exception" --> <variable name="ignore-postfix" select="'Exception'"/> <!-- Length of class name without (assumed) postfix. --> <variable name="clipped-length" select="string-length($exc/@class) - string-length($ignore-postfix)" /> <!-- Fully qualified exception name, less "Exception" postfix. --> <variable name="clipped"> <choose> <!-- No ends-with function... --> <!-- Characters in string numbered from 1, hence gatepost... --> <when test="substring($exc/@class, 1 + $clipped-length) = $ignore-postfix" > <value-of select="substring($exc/@class, 1, $clipped-length)"/> </when> <otherwise> <value-of select="$exc/@class"/> </otherwise> </choose> </variable> <call-template name="class-name"> <with-param name="qualified-class-name" select="$clipped"/> </call-template> </template> </stylesheet>
messageKeyUnknown=[Unkown message key ''{0}'']
messageNotString=[Message ''{1}'' for key ''{0}'' is not a String]
package org.apache.geronimo.utils;
/**
* For objects (typically exceptions) that can return locale dependent messages.
*/
public interface LocalizedMessage {
public String getLocalizedMessage(java.util.Locale locale);
}
