Saturday, March 20, 2010

Dyanmic ResourceBundles in CDI

So I consider this blog post more of a"Part 2" from my last blog post.

If you're anything like me, you like using ResourceBundles but find them to be an annoyance when it comes to using the same bundle in both your view and actions. I'm attempting to, with this blog post, make that a lot simpler for you.

What are resource bundles? java.util.ResourceBundle is an abstract class in Java SE that is Locale aware. It's an easy way to add i18n support in your application. You can use it to load "bundles" either programatically or from a properties file. In this example, I will provide you with some code that loads the bundle from a properties file, have an optional annotation that can specify what bundle and what locale, as well as code that dynamically looks up Locales.

Just like last time, we'll need to have a Producer method. In this example, the annotation is optional (as we assume that this code can handle the injection of any instance of java.util.ResourceBundle) and there will be an optional secondary producer for the Locale itself.

So first, the first draft of the Producer method:



@Produces
public ResourceBundle produceResourceBundle(InjectionPoint ip) {
Class container = ip.getMember().getDeclaringClass();
String baseName = container.getCanonicalName().replace(".","/");
Locale locale = Locale.getDefault();
return ResourceBundle.getBundle(baseName, locale);
}


Now, clearly this producer doesn't work that well, but it's a start. Using this, you can use @Inject ResourceBundle bundle; which if it is placed inside of a class com.tad.cdi.comps.Bundler, will look for a bundle with the same name; com/tad/cdi/comps/Bundler. Good so far, right?

Now I'm going to introduce the optional qualifier, @Bundle. Using @Bundle, you can specify a runtime dependency on either a different ResourceBundle (then the one being injected into) or a different locale.

Bundle.java:

@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Bundle {
public String baseName() default "";
public String locale() default "";
}


The Producer method to support this grows a bit, as we now support @Bundle, @Bundle(baseName="") @Bundle(locale="") and @Bundle(baseName="",locale="") so we have to add this logic to the producer.

A note for those not familiar with them: Locales, in string format, take the shape of language_Country_qualifier, so you can write something as simple as "en", "sp_US", or even "fr_CA_with.napoleon.dialect" as your Bundle.

An appropriate producer method would look something like this:


@Produces
public ResourceBundle produceResourceBundle(InjectionPoint ip) {
Bundle b = null;
if (ip.getAnnotated().isAnnotationPresent(Bundle.class)) {
b = ip.getAnnotated().getAnnotation(Bundle.class);
}
Class container = ip.getMember().getDeclaringClass();
String baseName = (b == null || b.baseName().equals("")) ? container.getCanonicalName() : b.baseName();
Locale locale = null;
if (b == null) {
locale = Locale.getDefault();
} else if (b.locale().equals("") || b.locale().equals("default")) {
locale = Locale.getDefault();
} else {
String[] lpieces = b.locale().split("_", 3);
switch (lpieces.length) {
case 0:
locale = Locale.getDefault();
break;
case 1:
locale = new Locale(lpieces[0]);
break;
case 2:
locale = new Locale(lpieces[0], lpieces[1]);
break;
case 3:
locale = new Locale(lpieces[0], lpieces[1], lpieces[2]);
break;
default:
locale = Locale.getDefault();
break;
}
}
baseName = baseName.replace(".", "/");
return ResourceBundle.getBundle(baseName, locale);
}


What we're doing with this code, we allow a a baseName to be specified in the @Bundle, this can take the form of "some.dotted.expression" or even "some/path/expression," meaning the bundle can be anywhere - doesn't need to be one dedicated to this class. Note that the behavior of @Bundle if no baseName is found is to use the enclosing class. Locale works similarly as well, you can specify locale="some_locale_expression" to load the locale, or leave it blank to load the default locale.

Still, this may not do everything that you need. If you're like me, you have some object that contains the locale of the current HTTP Session/User. And it may look something like this:


@SessionScoped
public class User {
...
public Locale getUsersLocale() { return myLocale; }
...
}


Well, if you've already gotten that part, then you're almost done. Using the BeanManager interface in CDI, you can actually dynamically load this Locale using BeanManager.getReference. First thing you need to do is add a Producer equivalent to the above method. Preferably, since User is already a SessionScoped CDI ManagedBean, the producer will go into the same method. Now, you do need to remember to handle the rules of CDI Producers - can't return null. So here's how to add the necessary logic to produce the Locale:


@SessionScoped
public class User {
...
public Locale getUsersLocale() { return myLocale; }
@Produces public Locale produceSessionUsersLocale() { return myLocale; }
...
}


What this now does is produce a Dependent Locale into your context. You can then modify your ResourceBundle producer in the following way; note that I have added logic that works even if this producer is absent, based on the previous steps.


@Inject
BeanManager beanManager;

@Produces
public ResourceBundle produceResourceBundle(InjectionPoint ip) {
Bundle b = null;
if (ip.getAnnotated().isAnnotationPresent(Bundle.class)) {
b = ip.getAnnotated().getAnnotation(Bundle.class);
}
Class container = ip.getMember().getDeclaringClass();
String baseName = (b == null || b.baseName().equals("")) ? container.getCanonicalName() : b.baseName();
Locale locale = null;
try {
Bean localeBean = (Bean) beanManager.getBeans(Locale.class).iterator().next();
CreationalContext cc = beanManager.createCreationalContext(localeBean);
Locale producedLocale = (Locale) beanManager.getReference(localeBean, Locale.class, cc);
locale = producedLocale;
} catch (Exception e) {
//not sure what to do here yet.
System.out.println("Caught an exception trying to load a ResourceBundle");
if (b == null) {
locale = Locale.getDefault();
} else if (b.locale().equals("") || b.locale().equals("default")) {
locale = Locale.getDefault();
} else {
String[] lpieces = b.locale().split("_", 3);
switch (lpieces.length) {
case 0:
locale = Locale.getDefault();
break;
case 1:
locale = new Locale(lpieces[0]);
break;
case 2:
locale = new Locale(lpieces[0], lpieces[1]);
break;
case 3:
locale = new Locale(lpieces[0], lpieces[1], lpieces[2]);
break;
default:
locale = Locale.getDefault();
break;
}
}
}
baseName = baseName.replace(".", "/");
return ResourceBundle.getBundle(baseName, locale);
}


So as you can see, we are attempting to lookup the Locale in the current context. If we find one, we always use that Locale; otherwise we use the original logic.

Now obviously, this guide wouldn't be too useful unless we had some Arquillian test cases.

The code is located in the same project as the previous post, but here are the essentials.

A test producer object, this can produce Locale as needed:


public class LocaleProducer {
@Produces
public Locale produceSomeLocale() {
System.out.println("Have a request for Locale English...");
return Locale.ENGLISH;
}
}


I also wrote two tests, one that uses the Locales with and without @Bundle, another that uses the producer style.

To verify the logic without Producing Locales:


@Deployment
public static JavaArchive createDeployment() {
return Archives.create("test.jar", JavaArchive.class)
.addClass(BundleProducer.class)
.addClass(Bundle.class)
.addResource("com/tad/cdi/mods/properties/ResourceBundleInjectTest.properties")
.addResource("com/tad/cdi/mods/properties/Special_sp.properties")
.addManifestResource("META-INF/beans.xml",
ArchivePaths.create("beans.xml"));
}

@Inject ResourceBundle bundle;

@Inject @Bundle(baseName="com/tad/cdi/mods/properties/Special",locale="sp")
ResourceBundle bundleSpecialSP;

@Test
public void testBundleContents() {
assertEquals("world",bundle.getString("hello"));
}

@Test
public void testBundleSpecified() {
assertEquals("bottom",bundleSpecialSP.getString("bob"));
}


And to test including the ability to produce locales:


@Deployment
public static JavaArchive createDeployment() {
return Archives.create("test.jar", JavaArchive.class)
.addClass(BundleProducer.class)
.addClass(Bundle.class)
.addClass(LocaleProducer.class)
.addResource("com/tad/cdi/mods/properties/ProducedLocaleTest_en.properties")
.addManifestResource("META-INF/beans.xml",
ArchivePaths.create("beans.xml"));
}

@Inject ResourceBundle bundle;

@Test
public void testBundleContents() {
assertEquals("world",bundle.getString("hello"));
}


So there you have it, enjoy your ResourceBundles!

1 comment:

  1. It's a really good approach. Just what I was looking for.

    Thank you!

    ReplyDelete