Database resource bundle in a JSF application

Goal

To have a JSF application internationalized using a database resource bundle

Description

This recipe explains the basic steps that are required to create a database resource bundle solution and to use it in a JSF application

How to

The following are the steps necessary to make this recipe work:

  1. Create the resource bundle extension that will delegate the resources lookup in a database control
  2. Create the necessary extensions for different locales (this is the ugliest part of the solution but it is necessary; on the other hand, it is really easy to generate a couple of classes for different locales and this recipe will show you how)
  3. Create the database tables and the corresponding JPA entities
  4. Create the resource bundle control that will get the contents from the database
  5. Define the resource bundle in the faces-config file

First, we will define the resource bundle class:

import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
public class DBResourceBundle extends ResourceBundle {

  protected static final String BUNDLE_NAME = DBResourceBundle.class.getName();

  public DBResourceBundle() {
    this(Locale.getDefault());
  }

  public DBResourceBundle(final Locale locale) {
    setParent(ResourceBundle.getBundle(BUNDLE_NAME, locale, new DBControl()));
  }

  @Override
  protected Object handleGetObject(String key) {
    return parent.getObject(key);
  }

  @Override
  public Enumeration getKeys() {
    return parent.getKeys();
  }
}

The previous class provides the implementation for the default locale. The most important part is the setParent() call inside the constructor with a locale as parameter where, basically, the resource bundle delegates the resource lookup in a parent where the implementation is delegated to an instance of the DBControl class.

Next, we will create several extensions to the previous class, one for each of the locales we want to support in our application. Each class will be similar to the following:

import java.util.Locale;

/**
 *
 * @author Paulo Zenida - Linkare TI
 *
 */
public class DBResourceBundle_pt extends DBResourceBundle {

  public DBResourceBundle_pt() {
    super(new Locale("pt"));
  }
}

The previous class was generated with the help of the shell scripts that follows:

  • generate_db_resource_bundles.sh
    #!/bin/bash
    
    locales=("pt" "en" "es" "fr" "de" "aa" "ab" "af" "am" "ar" "as" "ay" "az" 
    "ba" "be" "bg" "bi" "bn" "bo" "br" "ca" "co" "cs" "cy" "da" "dz" "el" "eo" 
    "et" "eu" "fa" "fi" "fj" "fo" "fy" "ga" "gd" "gl" "gn" "gu" "ha" "hi" "hr" 
    "hu" "hy" "ia" "ie" "ik" "in" "is" "it" "iw" "ja" "ji" "jw" "ka" "kk" "kl" 
    "km" "kn" "ko" "ks" "ku" "ky" "la" "ln" "lo" "lt" "lv" "mg" "mi" "mk" "ml" 
    "mn" "mo" "mr" "ms" "mt" "my" "na" "ne" "nl" "no" "oc" "om" "or" "pa" "pl" 
    "ps" "qu" "rm" "rn" "ro" "ru" "rw" "sa" "sd" "sg" "sh" "si" "sk" "sl" "sm" 
    "sn" "so" "sq" "sr" "ss" "st" "su" "sv" "sw" "ta" "te" "tg" "th" "ti" "tk" 
    "tl" "tn" "to" "tr" "ts" "tt" "tw" "uk" "ur" "uz" "vi" "vo" "wo" "xh" "yo" 
    "zh" "zu")
    for locale in "${locales[@]}"
    do
    	./generate_db_resource_bundle.sh $locale
    done
  • generate_db_resource_bundle.sh
    #!/bin/bash
    
    filename="DBResourceBundle_"${1}".java";
    echo "package com.linkare.efragment.bundle;" > $filename
    echo "" >> $filename
    echo "import java.util.Locale;" >> $filename
    echo "" >> $filename
    echo "/**" >> $filename
    echo " *" >> $filename 
    echo " * @author Paulo Zenida - Linkare TI" >> $filename
    echo " *" >> $filename
    echo " */" >> $filename
    echo "public class DBResourceBundle_"${1}" extends DBResourceBundle {" >> $filename
    echo "" >> $filename
    echo "    public DBResourceBundle_"${1}"() {" >> $filename
    echo "	super(new Locale(\""${1}"\"));" >> $filename
    echo "    }" >> $filename
    echo "}" >> $filename

The database layer is implemented using JPA with the entities ResourceBundle, which constitutes the bundle name and locale, and a list of associated ResourceMessage entities, which represent the actual message key and value for each bundle.

First, the ResourceBundle implementation class (it extends from a DefaultDomainObject class which is a JPA mapped superclass that defines a primary key of type long and a version attribute of type long too – the code is not shown because it is not relevant for this recipe):

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;

import com.linkare.commons.jpa.DefaultDomainObject;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
@Entity
@Table(name = "resource_bundle")
@XmlRootElement
@NamedQueries( { 
  @NamedQuery(name = ResourceBundle.QUERYNAME_FIND_ALL, 
                            query = ResourceBundle.QUERY_FIND_ALL),
  @NamedQuery(name = ResourceBundle.QUERYNAME_COUNT_ALL, 
              query = ResourceBundle.QUERY_COUNT_ALL),
  @NamedQuery(name = ResourceBundle.QUERYNAME_FIND_BY_LOCALE, 
              query = ResourceBundle.QUERY_FIND_BY_LOCALE) 
  }
)
public class ResourceBundle extends DefaultDomainObject {

  private static final long serialVersionUID = 1L;

  public static final String QUERY_PARAM_LOCALE = "locale";

  public static final String QUERYNAME_FIND_ALL = "ResourceBundle.findAll";
  public static final String QUERY_FIND_ALL = "Select r from ResourceBundle r";

  public static final String QUERYNAME_COUNT_ALL = "ResourceBundle.countAll";
  public static final String QUERY_COUNT_ALL = "select count(r) from ResourceBundle r";

  public static final String QUERYNAME_FIND_BY_LOCALE = "ResourceBundle.findByLocale";
  public static final String QUERY_FIND_BY_LOCALE = "select r from ResourceBundle r 
                 join fetch r.messages m where r.locale = :" + QUERY_PARAM_LOCALE;

  @Column(name = "locale")
  private String locale;

  @Column(name = "basename")
  private String basename;

  @Column(name = "last_modified")
  private Long lastModified;

  @OneToMany(mappedBy = "bundle")
  private List messages;

  /**
   * @return the locale
   */
  public String getLocale() {
    return locale;
  }

  /**
   * @param locale
   *            the locale to set
   */
  public void setLocale(String locale) {
    this.locale = locale;
  }

  /**
   * @return the basename
   */
  public String getBasename() {
    return basename;
  }

  /**
   * @param basename
   *            the basename to set
   */
  public void setBasename(String basename) {
    this.basename = basename;
  }

  /**
   * @return the lastModified
   */
  public Long getLastModified() {
    return lastModified;
  }

  /**
   * @param lastModified
   *            the lastModified to set
   */
  public void setLastModified(Long lastModified) {
    this.lastModified = lastModified;
  }

  /**
   * @return the messages
   */
  public List getMessages() {
    if (messages == null) {
      messages = new ArrayList();
    }
    return messages;
  }

  /**
   * @param messages
   *            the messages to set
   */
  public void setMessages(List messages) {
    this.messages = messages;
  }
}

And now comes the ResourceMessage implementation class:

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;

import com.linkare.commons.jpa.DefaultDomainObject;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
@Entity
@Table(name = "resource_message")
@XmlRootElement
@NamedQueries({ 
  @NamedQuery(name = ResourceMessage.QUERYNAME_FIND_ALL, 
              query = ResourceMessage.QUERY_FIND_ALL),
  @NamedQuery(name = ResourceMessage.QUERYNAME_COUNT_ALL, 
              query = ResourceMessage.QUERY_COUNT_ALL) })
public class ResourceMessage extends DefaultDomainObject {

  private static final long serialVersionUID = 1L;

  public static final String QUERYNAME_FIND_ALL = "ResourceMessage.findAll";
  public static final String QUERY_FIND_ALL = "Select r from ResourceMessage r";

  public static final String QUERYNAME_COUNT_ALL = "ResourceMessage.countAll";
  public static final String QUERY_COUNT_ALL = "select count(r) from ResourceMessage r";

  @Column(name = "message_key")
  private String key;

  @Column(name = "message_value")
  private String value;

  @ManyToOne
  @JoinColumn(name = "key_bundle")
  private ResourceBundle bundle;

  /**
   * @return the key
   */
  public String getKey() {
    return key;
  }

  /**
   * @param key
   *            the key to set
   */
  public void setKey(String key) {
    this.key = key;
  }

  /**
   * @return the value
   */
  public String getValue() {
    return value;
  }

  /**
   * @param value
   *            the value to set
   */
  public void setValue(String value) {
    this.value = value;
  }

  /**
   * @return the bundle
   */
  public ResourceBundle getBundle() {
    return bundle;
  }

  /**
   * @param bundle
   *            the bundle to set
   */
  public void setBundle(ResourceBundle bundle) {
    this.bundle = bundle;
  }
}

The previous JPA entities map to the following tables definition:

mysql> desc resource_bundle;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| version       | int(11)      | YES  |     | NULL    |                |
| basename      | varchar(255) | YES  |     | NULL    |                |
| last_modified | bigint(20)   | YES  |     | NULL    |                |
| locale        | varchar(255) | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> desc resource_message;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| version       | int(11)      | YES  |     | NULL    |                |
| message_key   | varchar(255) | YES  |     | NULL    |                |
| message_value | varchar(255) | YES  |     | NULL    |                |
| key_bundle    | bigint(20)   | YES  | MUL | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Now, it is time for the actual resource messages lookup, in the database specific control class:

import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListResourceBundle;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linkare.efragment.jpa.model.bundle.ResourceMessage;
import com.linkare.efragment.service.ResourceBundleService;
import com.linkare.efragment.service.ejb.ResourceBundleServiceLocal;
import com.linkare.efragment.util.LookupUtils;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
public class DBControl extends Control {

  private Logger logger = LoggerFactory.getLogger(DBControl.class);

  @Override
  public List getFormats(String baseName) {
    if (baseName == null) {
      throw new NullPointerException();
    }
    return Arrays.asList("db");
  }

  @Override
  public ResourceBundle newBundle(String baseName, Locale locale, String format, 
                        ClassLoader loader, boolean reload) throws IllegalAccessException,
                                                            InstantiationException, IOException {
    if ((baseName == null) || (locale == null) || (format == null) || (loader == null)) {
      throw new NullPointerException();
    }
    ResourceBundle bundle = null;
    if (format.equals("db")) {
      bundle = new EfragmentResourceBundle(locale);
    }
    return bundle;
  }

  /**
   * Our own implementation of a resource bundle inspired on the 
   *  ListResourceBundle class with a change so that getting a non existing key 
   * does not result in a MissingResourceException being thrown but, instead, 
   * returning the passed in key.
   */
  protected class EfragmentResourceBundle extends ListResourceBundle {

    private Locale locale;

    /**
     * ResourceBundle constructor with locale
     * 
     * @param locale
     */
    public EfragmentResourceBundle(final Locale locale) {
      this.locale = locale;
    }

    /**
     * Returns an array in which each item is a pair of objects in an Object array. 
     * The first element of each pair is the key, which must be a String, and the 
     * second element is the value associated with that key. See the class description 
     * for details.
     * 
     * @return an array of an Object array representing a key-value pair.
     */
    protected Object[][] getContents() {
      try {
        final ResourceBundleService resourceBundleService = LookupUtils.
            lookupWithinApp(ResourceBundleServiceLocal.BEAN_NAME,
	    ResourceBundleServiceLocal.class.getName());
	final com.linkare.efragment.jpa.model.bundle.ResourceBundle bundle = 
            resourceBundleService.findResourceBundle(locale);
	if (bundle != null) {
	  final List resources = bundle.getMessages();
	  Object[][] all = new Object[resources.size()][2];
	  int i = 0;
	  for (Iterator it = resources.iterator(); it.hasNext();) {
	    ResourceMessage resource = it.next();
	    all[i] = new Object[] { resource.getKey(), resource.getValue() };
	    i++;
	  }
	  return all;
	}
      } catch (final Exception e) {
        logger.error("Problems initializing the db control for {}", locale, e);
      }
      return new Object[][] {};
    }
  }
}

As requested by a reader of this post, here goes more details on the LookupUtils:

/**
 *
 * @author Paulo Zenida - Linkare TI
 * @author Artur Correia - Linkare TI
 * 
 */
public final class LookupUtils {

  public enum JNDILookupNameSpaces {
    JAVA_GLOBAL("java:global%s/%s/%s%s") {

      @Override
      public String format(final String applicationName, final String moduleName, 
                           final String beanName, final String fullyQualifiedInterfaceName) {
        final String appName = applicationName != null ? "/" + applicationName : "";
        final String interfaceName = fullyQualifiedInterfaceName != null ? "!" + fullyQualifiedInterfaceName : "";
        return String.format(getJndiFormat(), appName, moduleName, beanName, interfaceName);
            }
        },

    JAVA_APP("java:app/%s/%s%s") {

      @Override
      public String format(final String applicationName, final String moduleName, 
                           final String beanName, final String fullyQualifiedInterfaceName) {
        final String interfaceName = fullyQualifiedInterfaceName != null ? "!" + fullyQualifiedInterfaceName : "";
        return String.format(getJndiFormat(), moduleName, beanName, interfaceName);
      }
    },

    JAVA_MODULE("java:module/%s%s") {

      @Override
      public String format(final String applicationName, final String moduleName, final String beanName, final String fullyQualifiedInterfaceName) {
        final String interfaceName = fullyQualifiedInterfaceName != null ? "!" + fullyQualifiedInterfaceName : "";
        return String.format(getJndiFormat(), beanName, interfaceName);
      }
    };

    private final String jndiFormat;

    private JNDILookupNameSpaces(final String jndiFormat) {
      this.jndiFormat = jndiFormat;
    }

    protected String getJndiFormat() {
      return jndiFormat;
    }

    public abstract String format(final String applicationName, final String moduleName, 
                                  final String beanName, final String fullyQualifiedInterfaceName);
  }

  private static final String USER_TRANSACTION_LOOKUP_NAME = "java:comp/UserTransaction";

  private static final String MODULE_NAME_LOOKUP_PROPERTY = "java:module/ModuleName";

  private LookupUtils() {
  }

  public static UserTransaction lookupUT() {
    try {
      final InitialContext ic = new InitialContext();
      return (UserTransaction) ic.lookup(USER_TRANSACTION_LOOKUP_NAME);
    } catch (NamingException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public static  T lookupWithinApp(final String beanName, 
                                   final String fullyQualifiedInterfaceName) {

    final String module = genericLookup(MODULE_NAME_LOOKUP_PROPERTY);

    if (module != null) {
      final String jndiLookupName = JNDILookupNameSpaces.JAVA_APP.format(null, module, beanName, fullyQualifiedInterfaceName);

      return genericLookup(jndiLookupName);
    } else {
      return lookupWithinModule(beanName, fullyQualifiedInterfaceName);
    }
  }

  public static  T lookupWithinModule(final String beanName, 
                                      final String fullyQualifiedInterfaceName) {
    final String jndiLookupName = JNDILookupNameSpaces.JAVA_MODULE.format(null, null, beanName, fullyQualifiedInterfaceName);
    return genericLookup(jndiLookupName);
  }

  @SuppressWarnings("unchecked")
  private static  T genericLookup(final String jndiName) {
    try {
      final InitialContext context = new InitialContext();
      return (T) context.lookup(jndiName);
    } catch (NamingException e) {
      throw new RuntimeException(e);
    }
  }
}

and ResourceBundleServiceLocal. This class is merely a local business interface for an EJB containing the CRUD operations regarding the ResourceBundle entity, as follows:

@Local
public interface ResourceBundleServiceLocal extends ResourceBundleService {
  public static final String BEAN_NAME = "ResourceBundleServiceImpl";
}

The previous implementation defines the “db” format as the only format that is supported by the implementation and the actual implementation is delegated to an extension of the class ListResourceBundle where the method getContents() is implemented in a way that the ResourceBundleService, which is the local business interface of an EJB that fetches persistent ResourceBundle instances from the database, according to the locale passed in (this implementation does not take into account the name of the resource bundle in the database) and iterates through the ResourceMessage instances associated to it.

Finally, all we have left to do is to specify the resource bundle as the one that should be used in our JSF application, through its definition in the faces-config.xml file:

...
  <application>
    <resource-bundle>
      <base-name>com.linkare.efragment.bundle.DBResourceBundle</base-name>
      <var>messages</var>
    </resource-bundle>
    <locale-config>
      <default-locale>pt</default-locale>
      <supported-locale>pt</supported-locale>
      <supported-locale>en</supported-locale>
      ...
    </locale-config>
  </application>
...

Additionally, and in order to make it possible to reload the resource messages contained in the database and make the application be possible to change its messages without redeploy, an application scoped class observing a CDI event being thrown (the trigger of the event, although not shown, should be assumed to be thrown on an interface where the user is allowed to change the persistent resource messages values) was supplied that will clear the cached resources and force a re-fetch from the database the next time a resource bundle key is asked for:

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Named;

import com.linkare.efragment.cdi.event.DBResourceBundleReloadEvent;
import com.sun.faces.application.ApplicationAssociate;
import com.sun.faces.application.ApplicationResourceBundle;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
@Named
@ApplicationScoped
public class DBResourceBundleReloader implements Serializable {

  private static final long serialVersionUID = 1L;

  public void onReload(@Observes final DBResourceBundleReloadEvent bundleReloadEvent) {
    ResourceBundle.clearCache(Thread.currentThread().getContextClassLoader());

    ApplicationResourceBundle appBundle = ApplicationAssociate.getCurrentInstance().
                         getResourceBundles().get(DBResourceBundle.class.getName());
    Map resources = getFieldValue(appBundle, "resources");
    resources.clear();
  }

  @SuppressWarnings("unchecked")
  private static  T getFieldValue(Object object, String fieldName) {
    try {
      Field field = object.getClass().getDeclaredField(fieldName);
      field.setAccessible(true);
      return (T) field.get(object);
    } catch (Exception e) {
      return null;
    }
  }
}

Notice, in the previous implementation, the need to refresh the JSF application bundle as well, because JSF also caches the loaded resource bundles.

As an improvement, you can also add an AspectJ aspect that advises the call to java.util.ResourceBundle.getObject() and java.util.ResourceBundle.getString() so that the exception MissingResourceException is not thrown and a resource message prefixed and suffixed by the string “???” is returned instead:

import java.util.MissingResourceException;

/**
 * @author Paulo Zenida - Linkare TI
 */
public aspect MissingResourceExceptionHandler {

  private static final String MISSING_RESOURCE_DELIMITER = "???";

  Object around(String key) : 
    call(public Object java.util.ResourceBundle.getObject(String)) && 
    args(key) {
    try {
      return proceed(key);
    } catch (final MissingResourceException e) {
      return MISSING_RESOURCE_DELIMITER + key + MISSING_RESOURCE_DELIMITER;
    }
  }

  String around(String key) : 
    call(public String java.util.ResourceBundle.getString(String)) && 
    args(key) {
    try {
      return proceed(key);
    } catch (final MissingResourceException e) {
    return MISSING_RESOURCE_DELIMITER + key + MISSING_RESOURCE_DELIMITER;
    }
  }
}

Explanations

The previous recipe was implemented in a framework developed at Linkare named internally as eFragment (because it takes advantage of the Servlet 3.0 web fragments together with CDI to create a modular and extensible Web application) and is already serving different applications successfully. The implementation makes it possible to load all labels from within the database and even to change those labels dynamically without the need to restart the server through listening to CDI events that, on triggering, will clear and thus force reloading resource bundles.

Advertisements

5 comments

  1. By the way, in case you need the implementation of DBResourceBundleReloadEvent, that implementation is the following:

    public class DBResourceBundleReloadEvent implements Serializable {

    private static final long serialVersionUID = 1L;

    private final String locale;

    public DBResourceBundleReloadEvent(String locale) {
    super();
    this.locale = locale;
    }

    /**
    * @return the locale
    */
    public String getLocale() {
    return locale;
    }
    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s