portals-jetspeed-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a..@apache.org
Subject cvs commit: jakarta-jetspeed-2/jetspeed-api/src/java/org/apache/jetspeed/i18n KeyedMessage.java CurrentLocale.java
Date Mon, 07 Mar 2005 13:57:43 GMT
ate         2005/03/07 05:57:43

  Added:       jetspeed-api/src/java/org/apache/jetspeed/i18n
                        KeyedMessage.java CurrentLocale.java
  Log:
  A new message definition and automatic translation facility: KeyedMessage (and CurrentLocale)
  See: http://issues.apache.org/jira/browse/JS2-219
  
  Revision  Changes    Path
  1.1                  jakarta-jetspeed-2/jetspeed-api/src/java/org/apache/jetspeed/i18n/KeyedMessage.java
  
  Index: KeyedMessage.java
  ===================================================================
  /*
   * Copyright 2000-2004 The Apache Software Foundation.
   * 
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * 
   *      http://www.apache.org/licenses/LICENSE-2.0
   * 
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.jetspeed.i18n;
  
  import java.io.Serializable;
  import java.lang.reflect.Field;
  import java.lang.reflect.Modifier;
  import java.text.MessageFormat;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.ResourceBundle;
  
  import org.apache.jetspeed.exception.JetspeedException; // for javadoc ref
  import org.apache.jetspeed.security.SecurityException; // for javadoc ref
  
  /**
   * KeyedMessage provides an automatically derived i18n message key based on its static instance
definition and can be
   * used as comparable constant too.
   * <h3>Purpose</h3>
   * <p>
   * With a KeyedMessage a named constant message (format) can be statically defined which
automatically translate
   * themselves for a specific locale using an automatically derived ResourceBundle or even
a specified one.
   * </p>
   * <h3>Key derivation</h3>
   * <p>
   * Because KeyedMessages are created with a default message (format), even if no ResourceBundle
or its key is defined or
   * can't be found, message translation is still possible.
   * </p>
   * <p>
   * A KeyedMessage automatically derives the ResourceBundle lookup key from its (statically
defined) instance field name
   * using the following format: <br/><br/><code>
   *     &nbsp;&nbsp;&lt;containingClass.name&gt;.&lt;staticInstanceField.name&gt;
   * </code>
   * <br/>
   * </p>
   * <p>
   * The containingClass is derived at construction time by analyzing the StackTraceElements
of a thrown exception. This
   * <em><b>requires</b></em> the instance to be defined as a public
static field!
   * </p>
   * <p>
   * At first access, the key is resolved by inspecting the derived containingClass for the
<em>declared</em> field
   * defining this instance.
   * </p>
   * <p>
   * If the KeyedMessage instance <em><b>wasn't</b></em> defined as
public static field, the key can't be resolved and
   * message translation using a ResourceBundle won't be possible. Translation using the default
message will still work
   * though. Furthermore, this instance can't be used as comparable named constant as the
{@link #equals(Object)}method
   * will always return false in this case.
   * </p>
   * <h3>Default ResourceBundle name derivation</h3>
   * <p>
   * When the key of a KeyedMessage is resolved, the default ResourceBundle name for message
translation is retrieved from
   * the defined public static String field named {@link #KEYED_MESSAGE_BUNDLE_FIELD_NAME
"KEYED_MESSAGE_BUNDLE"}defined
   * in its containingClass or one of its superClasses or interfaces.
   * </p>
   * <p>
   * If this field cannot be found, the fully qualified name of the containingClass is used.
   * </p>
   * <p>
   * ResourceBundle names are cached in a Map for each containingClass and only derived for
the first KeyedMessage defined
   * in a containingClass.
   * </p>
   * <p>
   * <em>Again: only <b>resolved</b> instances can use a ResourceBundle
for message translation.</em>
   * </p>
   * <h3>Default Locale lookup</h3>
   * <p>
   * When a message is translated without a specified Locale, {@link CurrentLocale#get()}is
used to determine the default
   * Locale for the current Thread.
   * </p>
   * <p>
   * In Jetspeed, the <code>LocalizationValve</code> initializes the {@link CurrentLocale}
on each request.
   * KeyedMessages accessed within the context of an Jetspeed request therefore will always
be translated using the
   * current user Locale with the {@link #getMessage()}or {@link #toString()}methods.
   * </p>
   * <h3>Default ResourceBundle lookup</h3>
   * <p>
   * If a message translation is done using the default ResourceBundle name the ResourceBundle
is retrieved using the
   * ClassLoader of the containingClass. This means the bundle(s) must be provided in the
same context as from where the
   * containingClass is loaded. Usually (and preferably), this will be from the shared classpath
of the webserver.
   * </p>
   * <h3>MessageFormat parameters</h3>
   * <p>
   * MessageFormat patterns can also be used for a KeyedMessage.<br/>
   * With the {@link #create(Object[])}method a specialized copy of a KeyedMessage instance
can be created containing the
   * arguments to be used during message translation.
   * </p>
   * <p>
   * This new copy remains {@link equals(Object)}to its source and can still be used for named
constant comparison.
   * </p>
   * <p>
   * For simplified usage, three {@link #create(Object)},{@link #create(Object, Object)}and
   * {@link #create(Object, Object, Object)}methods are provided which delegate to {@link
#create(Object[])}with their
   * argument(s) transformed into an Object array.
   * </p>
   * <h3>Extending KeyedMessage</h3>
   * <p>
   * An statically defined KeyedMessage can be used as a "simple" named constant. <br/>If
additional metadata is required
   * like some kind of status, level or type indication, the KeyedMessage class can easily
be extended by providing a
   * specialized version of the {@link #create(KeyedMessage, Object[])}copy factory.
   * </p>
   * <h3>Usage</h3>
   * <p>
   * KeyedMessage has been used to replace the hardcoded {@link SecurityException} String
constants. <br/>The
   * ResourceBundle name used is defined by {@link JetspeedException#KEYED_MESSAGE_BUNDLE}
which is the superClass of
   * {@link SecurityException}.<br/>
   * <p>
   * <em>For a different ResourceBundle to be used for SecurityException messages a
KEYED_MESSAGE_BUNDLE field can be defined
   * in {@link SecurityException} too, overriding the one in {@link JetspeedException}.</em>
   * </p>
   * <p>
   * Example:
   * </p>
   * <pre>
   *       public class JetspeedException extends Exception {
   *           public static final String KEYED_MESSAGE_BUNDLE = &quot;org.apache.jetspeed.exception.JetspeedExceptionMessages&quot;;
   *           ...
   *    
   *           public String getMessage() {
   *                if ( keyedMessage != null ) {
   *                   return keyedMessage.getMessage(); // translated using current Locale
and default ResourceBundle
   *                }
   *                return super.getMessage();
   *           }
   *       }
   *    
   *       public class SecurityException extends JetspeedException {
   *           public static final KeyedMessage USER_DOES_NOT_EXIST = new KeyedMessage(&quot;The
user {0} does not exist.&quot;);
   *           ...
   *       }
   *    
   *       // resource file: org.apache.jetspeed.exception.JetspeedExceptionMessages_nl.properties
   *       org.apache.jetspeed.security.SecurityException.USER_DOES_NOT_EXIST = De gebruiker
{0} bestaat niet.
   *       ...
   *    
   *       public class UserManagerImpl implements UserManager {
   *           public User getUser(String username) throws SecurityException {
   *               ...
   *               if (null == userPrincipal) { 
   *                   throw new SecurityException(SecurityException.USER_DOES_NOT_EXIST.create(username));
   *               }
   *               ...
   *           }
   *           ...
   *       }
   *    
   *       // example get User
   *       try {
   *           User user = userManager.getUser(userName);
   *       } catch (SecurityException sex) {
   *           if ( SecurityException.USER_DOES_NOT_EXISTS.equals(sex.getKeyedMessage()) {
   *               // handle USER_DOES_NOT_EXISTS error
   *           }
   *       }    
   * </pre>
   * 
   * @author <a href="mailto:ate@douma.nu">Ate Douma</a>
   * @version $Id: KeyedMessage.java,v 1.1 2005/03/07 13:57:43 ate Exp $
   */
  public class KeyedMessage implements Serializable
  {
      /**
       * Static String Field name searched for in the class defining a KeyedMessage containing
the default resource bundle
       * to use for translation. <br/><em>Note: this Field is looked up using
definingClass.getField thus it may also be
       * defined in a superclass or interface of the definingClass.</em>
       */
      public static final String   KEYED_MESSAGE_BUNDLE_FIELD_NAME = "KEYED_MESSAGE_BUNDLE";
  
      /**
       * Key value for an unresolved KeyMessage.
       */
      private static final String  UNRESOLVED_KEY                  = KeyedMessage.class.getName()
+ ".<unresolved>";
  
      /**
       * Map caching default resource bundle names keyed on containingClass
       */
      private static final HashMap resourceNameMap                 = new HashMap();
  
      /**
       * Default message used when key couldn't be looked up in the default or a specified
resource bundle
       */
      private String               message;
  
      /**
       * Dynamically derived key based on the definingClass name, postfixed with the static
field name of this instance
       * </br>
       * 
       * @see #getKey()
       */
      private String               key;
  
      /**
       * Optional message format arguments which can only be set using a derived KeyedMessage
using the
       * {@link #create(Object[])}method(s).
       */
      private Object[]             arguments;
  
      /**
       * The class in which this instance is defined as a static Field.
       */
      private Class                containingClass;
  
      /**
       * Indicates if this instance could be {@link #resolve() resolved}.
       */
      private boolean              resolved;
  
      /**
       * Constructs a derived KeyedMessage from another KeyedMessage to provide additional
message format arguments.
       * 
       * @see #create(Object[])
       * @param source the KeyedMessage to derive this instance from
       * @param arguments this instance specific message format arguments
       */
      protected KeyedMessage(KeyedMessage source, Object[] arguments)
      {
          this.key = source.getKey();
          this.message = source.message;
          this.resolved = source.resolved;
          this.containingClass = source.containingClass;
          this.arguments = arguments;
      }
  
      /**
       * Constructs a new KeyedMessage which will dynamically derive its own {@link #getKey()}.
       * 
       * @param message the default message used when the {@link #getKey()}could not be found
in the default or a
       *            specified resource bundle.
       */
      public KeyedMessage(String message)
      {
          try
          {
              throw new Exception();
          }
          catch (Exception e)
          {
              StackTraceElement[] elements = e.getStackTrace();
              if (elements.length >= 2)
              {
                  String containingClassName = elements[1].getClassName();
                  try
                  {
                      containingClass = Thread.currentThread().getContextClassLoader().loadClass(containingClassName);
                  }
                  catch (ClassNotFoundException e1)
                  {
                      key = UNRESOLVED_KEY;
                  }
              }
          }
          this.message = message;
      }
  
      private String getResourceName()
      {
          synchronized (resourceNameMap)
          {
              return (String) resourceNameMap.get(containingClass);
          }
      }
  
      /**
       * @see KeyedMessage
       */
      private void resolve()
      {
          if (key == null)
          {
              // search for this instance as a statically declared field in the containingClass
to find out the name
              // to use.
              Field[] fields = containingClass.getDeclaredFields();
              for (int i = 0; i < fields.length; i++)
              {
                  try
                  {
                      if (fields[i].getType() == this.getClass() && Modifier.isStatic(fields[i].getModifiers())
                          && fields[i].get(null) == this)
                      {
                          // resolved: save the key
                          key = containingClass.getName() + "." + fields[i].getName();
                          resolved = true;
  
                          // Now derive the default resource bundle if not already done before
                          synchronized (resourceNameMap)
                          {
                              if (getResourceName() == null)
                              {
                                  // Find resource bundle name by looking up the statically
defined
                                  // KEYED_MESSAGE_BUNDLE_FIELD_NAME String field in the containingClass.
                                  String resourceName = null;
                                  try
                                  {
                                      Field field = containingClass.getField(KEYED_MESSAGE_BUNDLE_FIELD_NAME);
                                      if (field != null && field.getType() == String.class
                                          && Modifier.isStatic(field.getModifiers()))
                                      {
                                          resourceName = (String) field.get(null);
                                      }
                                  }
                                  catch (Exception e)
                                  {
                                  }
                                  if (resourceName == null)
                                  {
                                      // fallback to containingClass name as resource bundle
name
                                      resourceName = containingClass.getName();
                                  }
                                  resourceNameMap.put(containingClass, resourceName);
                              }
                          }
  
                          break;
                      }
                  }
                  catch (Exception e)
                  {
                  }
              }
              if (key == null)
              {
                  key = UNRESOLVED_KEY;
              }
          }
      }
  
      /**
       * Formats a message using MessageFormat if arguments are defined, otherwise simply
returns the argument.
       * 
       * @param message the message format
       * @return formatted message
       */
      private String format(String message)
      {
          if (arguments != null && arguments.length > 0)
          {
              return new MessageFormat(message).format(arguments);
          }
          else
          {
              return message;
          }
      }
  
      /**
       * Extendable KeyedMessage factory
       * 
       * @param source the source to copy from
       * @param arguments the optional message format arguments
       * @return copied instance with new arguments set
       */
      protected KeyedMessage create(KeyedMessage source, Object[] arguments)
      {
          return new KeyedMessage(this, arguments);
      }
  
      /**
       * Creates a derived KeyedMessage from this instance to provide additional message format
arguments. <br/>The new
       * instance will be {@link #equals(Object)}to this instance with only different arguments.
<br/><br/>Note: the
       * argument objects should be lightweight types and preferably Serializable instances
       * 
       * @param arguments The derived instance specific message format arguments
       * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message
format arguments
       */
      public KeyedMessage create(Object[] arguments)
      {
          return new KeyedMessage(this, arguments);
      }
  
      /**
       * Simplied version of {@link #create(Object[])}with only one argument
       * 
       * @param single message format argument
       * @see #create(Object[])
       * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message
format argument
       */
      public KeyedMessage create(Object o)
      {
          return create(new Object[] { o });
      }
  
      /**
       * Simplied version of {@link #create(Object[])}with only two arguments
       * 
       * @param single message format argument
       * @see #create(Object[])
       * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message
format arguments
       */
      public KeyedMessage create(Object o1, Object o2)
      {
          return create(new Object[] { o1, o2 });
      }
  
      /**
       * Simplied version of {@link #create(Object[])}with only three arguments
       * 
       * @param single message format argument
       * @see #create(Object[])
       * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message
format arguments
       */
      public KeyedMessage create(Object o1, Object o2, Object o3)
      {
          return create(new Object[] { o1, o2, o3 });
      }
  
      /**
       * Dynamically derived key based on the definingClass name, postfixed with the static
field name of this instance.
       * <br/><br/>Format: <br/><code>
       *     &nbsp;&nbsp;&lt;containingClass.name&gt;.&lt;staticInstanceField.name&gt;
       * </code>
       * <br/><br/>If this instance couldn't be resolved, generic value UNRESOLVED_KEY
will have been set.
       * 
       * @return derived key
       */
      public final String getKey()
      {
          resolve();
          return key;
      }
  
      /**
       * Loads and returns a Locale specific default ResourceBundle for this instance. <br/>If
this instance couldn't be
       * {@link #resolve() resolved}or the bundle couldn't be loadednull will be returned.
<br/>The ResourceBundle will
       * be loaded using the {@link #containingClass}its ClassLoader.
       * 
       * @param locale the Locale to lookup the locale specific default ResourceBundle
       * @return a Locale specific default ResourceBundle
       */
      public ResourceBundle getBundle(Locale locale)
      {
          resolve();
          if (resolved)
          {
              try
              {
                  return ResourceBundle.getBundle(getResourceName(), locale, containingClass.getClassLoader());
              }
              catch (RuntimeException e)
              {
              }
  
          }
          return null;
      }
  
      /**
       * Loads and returns the default ResourceBundle for this instance using the
       * {@link CurrentLocale#get() current Locale}.
       * 
       * @see #getBundle(Locale)
       * @see CurrentLocale
       * @return the default ResourceBundle for the current Locale
       */
      public ResourceBundle getBundle()
      {
          return getBundle(CurrentLocale.get());
      }
  
      /**
       * @return formatted message using the default ResourceBundle using the {@link CurrentLocale
current Locale}.
       * @see #getBundle()
       */
      public String getMessage()
      {
          return getMessage(getBundle());
      }
  
      /**
       * @param bundle a specific ResourceBundle defining this instance {@link #getKey() key}
       * @return formatted message using a specific ResourceBundle.
       */
      public String getMessage(ResourceBundle bundle)
      {
          resolve();
          String message = this.message;
          if (resolved && bundle != null)
          {
              try
              {
                  message = bundle.getString(key);
              }
              catch (RuntimeException e)
              {
                  // ignore: fallback to default message
              }
          }
          return format(message);
      }
  
      /**
       * @param locale a specific Locale
       * @return formatted message using the default ResourceBundle using a specific Locale.
       */
      public String getMessage(Locale locale)
      {
          return getMessage(getBundle(locale));
      }
  
      /**
       * @return the arguments defined for this {@link #create(Object[]) derived}instance
       * @see #create(Object[])
       */
      public Object[] getArguments()
      {
          return arguments;
      }
  
      /**
       * @param index argument number
       * @return an argument defined for this {@link #create(Object[]) derived}instance
       */
      public Object getArgument(int index)
      {
          return arguments[index];
      }
  
      /**
       * @return formatted message using the default ResourceBundle using the {@link CurrentLocale
current Locale}.
       * @see #getMessage()
       */
      public String toString()
      {
          return getMessage();
      }
  
      /**
       * @param otherObject KeyedMessage instance to compare with
       * @return true only if otherObject is a KeyedMessage {@link create(Object[]) derived}from
this instance (or visa
       *         versa) and (thus both are) {@link #resolve() resolved}.
       * @see #create(Object[])
       * @see #resolve()
       */
      public boolean equals(Object otherObject)
      {
          if (otherObject != null && otherObject instanceof KeyedMessage)
          {
              resolve();
              return (resolved && key.equals(((KeyedMessage) otherObject).getKey()));
          }
          return false;
      }
  }
  
  
  1.1                  jakarta-jetspeed-2/jetspeed-api/src/java/org/apache/jetspeed/i18n/CurrentLocale.java
  
  Index: CurrentLocale.java
  ===================================================================
  /*
   * Copyright 2000-2004 The Apache Software Foundation.
   * 
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * 
   *      http://www.apache.org/licenses/LICENSE-2.0
   * 
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.jetspeed.i18n;
  
  import java.util.Locale;
  
  /**
   * Maintains a Locale for the current Thread
   * 
   * @author <a href="mailto:ate@douma.nu">Ate Douma </a>
   * @version $Id: CurrentLocale.java,v 1.1 2005/03/07 13:57:43 ate Exp $
   */
  public final class CurrentLocale
  {
      private static ThreadLocal currentLocale = new ThreadLocal();
      
      private CurrentLocale()
      {    
      }
      
      /** @return the currently {@link #set(Locale) set} Locale in this Thread or Locale.getDefault()
otherwise
       */
      public static Locale get()
      {
          Locale locale = (Locale)currentLocale.get();
          return locale != null ? locale : Locale.getDefault();
      }
      
      /**
       * Sets a Locale for this Thread.
       * <br>
       * Use a null parameter to revert back to Locale.getDefault() 
       * @param locale Locale for this Thread 
       */
      public static void set(Locale locale)
      {
          currentLocale.set(locale);
      }
  }
  
  
  

---------------------------------------------------------------------
To unsubscribe, e-mail: jetspeed-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: jetspeed-dev-help@jakarta.apache.org


Mime
View raw message