Thursday, October 21, 2010

JSF Converter for SelectItem Value, made SIMPLE!

Ok, guys and gals. You're here either because you have no idea what a JSF converter is but you've been told that using one will fix your exception, or else you know exactly what one is and you're sick of writing a different converter for every friggin object that needs to be used in a menu in your app.

... Or maybe you're clueless about software development all together and stalking me for pleasure. Either way, welcome and read on!

The Universal JSF Converter
One converter to rule them all...

I've developed a single converter that can be applied to any JSF UISelect component requiring one. For example, selectOneMenu, selectManyListbox, selectOneRadio, etc etc.... I've developed a rough crack at what JSF should have included from the get go so that developers don't need to write converter code, converting their business objects into Strings for representation in a menu.

So we've got a list of Objects that we want to show in a SelectItem list. Some String value will need to be used in the "value" attribute of the rendered element.

<option value="?????" >Employee 1</option>

Why should we care what value is used?? Why doesn't JSF just assign some random identifier to each object? Really, there's no need to write a different converter for each object, depending on that object's primary key value, or other data. Any object can be converted to a String to display on the screen and then converted back into an object after being selected. Why does JSF force us to write a converter? There should be a better default converter. A universal default converter.

Enough hubbub, here's the solution: JsfUniversalConverter.java
private static final String objectCacheKey = "JSF_UNIVERSAL_CONVERTER_OBJECT_CACHE";

@Override
public Object getAsObject(FacesContext fc, UIComponent uic, String string) {
if (string == null || string.length() == 0) {
return null;
}
Object returnObject = getObjectCache(fc).get(string);
return returnObject;
}

@Override
public String getAsString(FacesContext fc, UIComponent uic, Object o) {
if (o == null) {
return "";
}
String returnString = null;
Map objectCache = getObjectCache(fc);
//search cacheMap to see if this has already been converted.
for (Map.Entry cacheEntry : objectCache.entrySet()) {
Object cachedObject = cacheEntry.getValue();
if (cachedObject==null) continue;
if ( o.equals(cachedObject) || o==cachedObject ) {
returnString = cacheEntry.getKey();
}
}
if (returnString==null) {
returnString = UUID.randomUUID().toString();
objectCache.put(returnString, o);
}
return returnString;
}

private Map getObjectCache(FacesContext fc) {
HttpSession session = (HttpSession) fc.getExternalContext().getSession(true);
Object object = session.getAttribute(objectCacheKey);
if (object!=null && object instanceof Map) {
return (Map) object;
} else {
Map objectCache = new HashMap();
session.setAttribute(objectCacheKey, objectCache);
return objectCache;
}
}

Be sure to plug this into your faces-config.xml:
<converter>
<converter-id>universal</converter-id>
<converter-class>my.package.JsfUniversalConverter</converter-class>

</converter>



And if you're really good, just build a jar file that contains the class and faces-config.xml, and include this jar file on every JSF project.

From here on out, you solve all of your menu conversion issues with ONE converter:
<h:selectOneMenu converter="universal" ...



Caveat: If you use this and receive "invalid value" errors:
  1. Remember to properly implement the equals and hashcode methods of your business objects
    OR
  2. Ensure that your managed bean providing the List is returning the exact same instances of the objects in the SelectItem value every time.

Exciting? Thank me by leaving a comment. Your comments are what I do all this for. Thanks!

No comments: