It is often useful to display a dynamic list of options for possible values of a bean field. One form of this is described in the Cook Book under Forms. However, it is possible to make the process somewhat less cumbersome than that and get the benefit of RIFE's validation and form building.
Consider a bean with a categoryId property that must be set to the ID of a valid Category object in the database. We want a <select> list that has the ID of each category as the option value and the name as the option text.
Since RIFE's <select> builder uses ResourceBundle objects to map option values to text, we use a custom ResourceBundle implementation:
package com.foo.util;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import com.uwyn.rife.tools.IteratorEnumeration;
/**
* Resource bundle that generates its key/value pairs from a map, with an
* optional prefix before each key.
*/
public class MapResourceBundle extends ResourceBundle {
private Map<String,String> values = new HashMap<String,String>();
/**
* Creates a new bundle with an empty set of key/value pairs.
*/
public MapResourceBundle() { }
/**
* Creates a new bundle with a set of key/value pairs from an existing
* Map.
*/
public MapResourceBundle(Map<? extends Object,String> pairs) {
for (Map.Entry<? extends Object, String> entry : pairs.entrySet()) {
values.put(entry.getKey().toString(), entry.getValue());
}
}
/**
* Creates a new bundle with a set of key/value pairs, prefixing each
* key with a property name and a colon.
*/
public MapResourceBundle(Map<? extends Object,String> pairs, String prefix) {
StringBuilder sb = new StringBuilder(prefix);
sb.append(':');
int prefixLen = sb.length();
for (Map.Entry<? extends Object, String> entry : pairs.entrySet()) {
sb.setLength(prefixLen);
sb.append(entry.getKey().toString());
values.put(sb.toString(), entry.getValue());
}
}
@Override
public Enumeration<String> getKeys() {
return new IteratorEnumeration(values.keySet().iterator());
}
@Override
protected Object handleGetObject(String key) {
return values.get(key);
}
}
To use this, we need a normal RIFE form tag in our template:
<r:v name="FORM:SELECT:categoryId"/>
And in the element, in addition to any other setup required for the form, we do:
Map<Integer, String> categoryMap = new HashMap<Integer, String>();
for (Category cat : categoryList) {
categoryMap.put(cat.getId(), cat.getName());
}
((Constrained) item).addConstraint(new ConstrainedProperty("categoryId")
.inList(categoryMap.keySet()));
template.addResourceBundle(new MapResourceBundle(categoryMap, "categoryId"));
generateForm(template, item);
print(template);
This will generate the desired HTML, selecting the current category ID (if any) as the default value just like RIFE's non-dynamic form generation would do.
If you're using a lot of dynamic <select> lists, you may want to abstract the above out into a utility method. Here's one possible implementation, which also supports adding an initial "please select a choice" style message to the list. (Note that this implementation will actually add that message to your map of possible values.)
package com.foo.util;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.uwyn.rife.site.Constrained;
import com.uwyn.rife.site.ConstrainedProperty;
import com.uwyn.rife.template.Template;
/**
* Helper methods for building dynamic select lists.
*/
public abstract class SelectListHelper {
private static double LONG_MAX = new Double(Long.toString(Long.MAX_VALUE));
private static double INT_MAX = new Double(Integer.toString(Integer.MAX_VALUE));
private static double SHORT_MAX = new Double(Short.toString(Short.MAX_VALUE));
private static double BYTE_MAX = new Double(Byte.toString(Byte.MAX_VALUE));
/**
* Generates a <select> HTML element for a bean field using an
* arbitrary set of values.
*
* @param template
* Template that contains the form to populate.
* @param bean
* Bean to associate with the select element.
* @param property
* Which bean property is being enumerated.
* @param selectItems
* Map of values to names; each element in this map will be added to
* the list of options.
*/
public static void prepareSelectList(Template template,
Object bean,
String property,
Map selectItems) {
prepareSelectList(template, bean, property, selectItems, null, null);
}
/**
* Generates a <select> HTML element for a bean field using an
* arbitrary set of values.
*
* @param template
* Template that contains the form to populate.
* @param bean
* Bean to associate with the select element.
* @param property
* Which bean property is being enumerated.
* @param selectItems
* Map of values to names; each element in this map will be added to
* the list of options.
* @param prefix
* Prefix to add to the property name, or null if none.
* @param firstItem
* Optional first item to add to the list, or null if none.
*/
public static void prepareSelectList(Template template,
Object bean,
String property,
Map selectItems,
String prefix,
String firstItem) {
List entries = new LinkedList();
Object firstItemObj = null;
if (null == prefix)
prefix = "";
entries.addAll(selectItems.keySet());
if (firstItem != null) {
if (selectItems.size() == 0) {
firstItemObj = "dummy";
}
else {
Object obj = entries.get(0);
do {
double rnd = Math.random();
if (obj instanceof Integer)
firstItemObj = (int)(rnd * INT_MAX);
else if (obj instanceof Long)
firstItemObj = (long)(rnd * LONG_MAX);
else if (obj instanceof Short)
firstItemObj = (short)(rnd * SHORT_MAX);
else if (obj instanceof Byte)
firstItemObj = (byte)(rnd * BYTE_MAX);
else if (obj instanceof Float)
firstItemObj = (float)rnd;
else if (obj instanceof Double)
firstItemObj = rnd;
else if (obj instanceof String)
firstItemObj = String.valueOf(rnd);
else {
try {
firstItemObj = obj.getClass().newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
} while (selectItems.containsKey(firstItemObj));
}
entries.add(0, firstItemObj);
selectItems.put(firstItemObj, firstItem);
}
((Constrained)bean).addConstraint(new ConstrainedProperty(property)
.inList(entries)
.notEqual(firstItemObj));
template.addResourceBundle(new MapResourceBundle(selectItems,
prefix + property));
}
}
Using this helper method, if we want a list with an instructional message as the first option, our element just needs to do the following:
Map<Integer, String> categoryMap = new HashMap<Integer, String>();
for (Category cat : categoryList) {
categoryMap.put(cat.getId(), cat.getName());
}
SelectListHelper.prepareSelectList(template, item, "categoryId", categoryMap,
null, "Please select a category.");
generateForm(template, item);
print(template);