Handling missing resources with CDI Events
In part 1, we created a simple application that made use of string resource bundles in JSF and in part 2 we extended it by using CDI to inject the resource provider into beans so we can re-use our code for accessing locale specific string based resources.
One benefit of using CDI to inject your beans instead of just creating them manually is the ability to utilize the event mechanism within those beans. In this example, we are going to fire an event when we discover a resource is missing.
- Start by creating a
MissingResourceInfo
class that stores the info regarding the missing resource (key and locale). This will be passed along with the event so the observer has all the info when it receives the event.public class MissingResourceInfo { private final String key; private final String locale; public MissingResourceInfo(String key, Locale locale) { this(key, locale.getDisplayName()); } public MissingResourceInfo(String key, String locale) { super(); this.key = key; this.locale = locale; } public String getKey() { return key; } public String getLocale() { return locale; } }
- We fire an event when we discover a resource key is missing from the locale specific properties file being used. In the
MessageProvider
class from part 2, we inject theEvent
instance in to a field and modify thegetValue
method to fire off the event when a key is missing.@RequestScoped public class MessageProvider { @Inject private Event<MissingResourceInfo> event; .... .... public String getValue(String key) { String result = null; try { result = getBundle().getString(key); } catch (MissingResourceException e) { result = "???" + key + "??? not found"; event.fire(new MissingResourceInfo(key, getBundle().getLocale())); } return result; } }
- Finally, we want to add an observer for that event that handles it when it is fired. To do this, we will add a new class for this purpose.
public class MissingResourceObserver { public void observeMissingResources(@Observes MissingResourceInfo info) { String template = "Oh noes! %s key is missing in locale %s"; String msg = String.format(template, info.getKey(), info.getLocale()); System.out.println(msg); } }
- Finally, to see it in action, we need to add bean getter to fetch a non-existing value from the bundle.
@Named @RequestScoped public class AnotherBean { public String getMissingResource() { return provider.getValue("ImNotHere"); } }
- We can see this in action by adding to our JSF page a call to this property
#{anotherBean.missingResource}
Results in the following being displayed in the console :
Oh noes! 'ImNotHere' key is missing from locale English (United States)
Remember, you can have as many event observers as you want so you can have one that just logs it to the console or one that emails if missing text strings are urgent (i.e. production). You could have it write the missing values to the database so during development cycles, the missing strings can be exported and put into the properties files. This is especially useful if you have third parties handling the translation where you can send them a list of missing items to translate in one big batch.
What about EL?
This works great when we are accessing resource bundles from Java, but what about when we are using EL expressions to access the values. JSF channels those expressions straight to the resource bundle without the ability to intercept them. One way to fix this would be to write our own EL expression resolver and see if the root of the expression maps to a resource bundle and if so try to obtain the value from the bundle and fire the event if the item is not found. This would work, but it seems overkill since every EL expression would end up going through the resolver.
Remember that the syntax for accessing resource strings in EL is #{bundlename.key}
, and Java Map
classes can also be accessed using the same syntax. What we can do is create a named bean that (barely) implements the map interface and in the get
method, it gets the value from the injected resource bundle. This means EL expressions will use our Map bean and we can delegate the call to the Message provider which will handle missing expressions.
- Start by creating a new bean that implements the
Map
interface. Most of the methods will throw exceptions that they are not implemented, we only really care about the method to get values.@Named("messages") @RequestScoped public class ResourceMap implements Map<String, String> { @Inject private MessageProvider provider; @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean containsKey(Object key) { return provider.getBundle().containsKey((String) key); } @Override public boolean containsValue(Object value) { throw new RuntimeException("Not implemented"); } @Override public String get(Object key) { return provider.getValue((String) key); } @Override public String put(String key, String value) { throw new RuntimeException("Not implemented"); } @Override public String remove(Object key) { throw new RuntimeException("Not implemented"); } .... .... .... }
If the interface method isn’t listed, assume it throws a “not implemented” exception. We inject the
MessageProvider
instance so we can re-use our message provider bean to fetch messages and fire the events. -
We gave the bean the name
messages
so if we go to our web page and add the following to our page :First name from map = #{messages.firstName}<br/> Missing name from map = #{messages.missingValue}<br/>
When we run our application and refresh the page, we get the value displayed for
messages.firstName
, and a missing value formessages.missingValue
. In our server log we getOh noes! missingValue key is missing in locale English (United States)
Since we re-used the
MessageProvider
bean, it fires the event when it cannot find a key value so even though we called it from the JSF page, it ended up being routed through to our message provider and the event being fired.
How you use this is up to you, but since the syntax is the same whether you use the Map or the JSF resource variable, you can switch between the two depending on whether it is a development or production environment. You may also use different event handlers depending on the deployment environment. Alternatively, you may be worried about performance in production, or the fact that you lose autocompletion if you use the map in development.
To switch implementations, just give either the JSF resource variable, in faces-config
or the MessageProvider
bean the name used for your resource string component. You can switch components without having to change either your code or your JSF pages.
Download the Maven source code for this project and run it locally by typing unzipping it and running mvn clean jetty:run
in the command line, and going to http://localhost:8080/resourcedemo/.
2 thoughts on “Handling missing resources with CDI Events”
Comments are closed.
Hi. I have two questions:
1. How can I track events connected with session (session context) with CDI (i.e. session start)
2. How can I instantiate selected class at the beggining of selected context (something like @Startup in seam 2)
Paul
In a nutshell, you can’t. You can observe some application level event of the BeanManager (post bean validation or something like that), but that is more of an internal event handler, you can’t actually use your own beans or get them injected at that point.
Here’s a link on the discussion of startup beans in the Weld extensions.