Coaxing Enums Into XPage Combo Boxes
Wed May 28 19:07:12 EDT 2014
I've been reworking Forms 'n' Views over the last couple days to use it as a development companion for the OpenNTF Design API, and one of the minor hurdles I ran across was presenting a getter/setter pair that expects a Java Enum to an XPages control (a combo box in this case, but it would be the same with other similar controls). I had assumed at first that it would simply not work - that the runtime would entirely balk at the conversion. However, it turned out that XPages does half the job for you: for displaying existing values, it will properly coax between the string form and the Enum constant. However, it breaks when actually going to set the value.
This whetted my appetite enough that I decided to try to see if I could write a small converter to finish the job in a properly generic way, and it turns out I could. To cut to the chase, I wrote this class:
package converter; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.el.ValueBinding; public class EnumBindingConverter implements Converter { @SuppressWarnings("unchecked") public Object getAsObject(final FacesContext facesContext, final UIComponent component, final String value) { ValueBinding binding = component.getValueBinding("value"); Class<? extends Enum> enumType = binding.getType(facesContext); return Enum.valueOf(enumType, value); } public String getAsString(final FacesContext facesContext, final UIComponent component, final Object value) { return String.valueOf(value); } }
The getAsString
method is your standard "I don't care about this" near-passthrough, but the getAsObject
portion does the work necessary. Because I'm binding to a Java bean with getters/setters expecting the appropriate Enum type (this would presumably also work with a DataObject
with a thoroughly-implemented getType
method), the ValueBinding
class is able to look up and provide back the expected Enum class. I then use the standard Enum.valueOf
method to let Java do the work of actually finding the appropriate Enum constant.
Using this, I'm able to write a control like this:
<xp:comboBox value="#{columnNode.sortOrder}"> <xp:this.converter><xp:converter converterId="enumBindingConverter"/></xp:this.converter> <xp:selectItem itemLabel="None" itemValue="${javascript:org.openntf.domino.design.DesignColumn.SortOrder.NONE}" /> <xp:selectItem itemLabel="Ascending" itemValue="${javascript:org.openntf.domino.design.DesignColumn.SortOrder.ASCENDING}"/> <xp:selectItem itemLabel="Descending" itemValue="${javascript:org.openntf.domino.design.DesignColumn.SortOrder.DESCENDING}"/> </xp:comboBox>
And it works! That allows you to bind to a getter/setter pair that expects an Enum without having to write in a shim to convert it to a String value beyond the generic converter.
You probably noticed the same thing I did, though: that's ugly as sin. Not only that, but it's a lot of redundant code when what you usually want to do is provide a selection of all available constants for the given Enum. To save myself a bit of future hassle and prettify my code a bit (at the expense of UI, but who cares about that? (I kid)). I wrote this DataObject
bean to accept an Enum class name and return a list suitable for use in a control:
package bean; import java.io.Serializable; import java.util.*; import javax.faces.context.FacesContext; import javax.faces.model.SelectItem; import org.openntf.domino.utils.Strings; import com.ibm.xsp.model.DataObject; import frostillicus.bean.*; @ManagedBean(name="enumItems") @ApplicationScoped public class EnumSelectItems implements Serializable, DataObject { private static final long serialVersionUID = 1L; public Class<?> getType(final Object key) { return List.class; } @SuppressWarnings("unchecked") public List<SelectItem> getValue(final Object key) { if(key == null) { throw new NullPointerException("key cannot be null."); } String className = String.valueOf(key); try { Class<? extends Enum> enumClass = (Class<? extends Enum>)FacesContext.getCurrentInstance().getContextClassLoader().loadClass(className); Enum[] vals = enumClass.getEnumConstants(); List<SelectItem> result = new ArrayList<SelectItem>(vals.length); for(Enum val : vals) { SelectItem item = new SelectItem(); item.setLabel(Strings.toProperCase(val.name())); item.setValue(val); result.add(item); } return result; } catch(Throwable t) { throw new RuntimeException(t); } } public boolean isReadOnly(final Object key) { return true; } public void setValue(final Object key, final Object value) { } }
It uses the @ManagedBean
implementation that I cobbled together a while back, but you could just as easily use the normal faces-config route. The way you use this is to pass in the class name of the Enum you want in a selectItems
component:
<xp:comboBox value="#{columnNode.sortOrder}" onchange="fnv_mark_dirty('#{id:viewPane}')"> <xp:this.converter><xp:converter converterId="enumBindingConverter"/></xp:this.converter> <xp:selectItems value="${enumItems['org.openntf.domino.design.DesignColumn$SortOrder']}"/> </xp:comboBox>
Though it only saves a few line in this case, it'd pay off more with larger Enums. It has the down side of being pretty naive about its conversions: it just does a basic first-character proper-casing of the Enum constant name, but there's no reason you couldn't add better human-friendly-ifying to it.
I'm not sure if I'll be able to satisfy all of my Java preferences when it comes to multi-value Enums, though. Java's generic type erasure means that runtime code wouldn't have a way to inspect the getters/setters to figure out the appropriate Enum type. Perhaps I can inspect the select items attached to the control to find the appropriate class.