Dashboard > RIFE > ... > Tips and Tricks > Dynamic select using dynamic constraints
RIFE Log In | Sign Up   View a printable version of the current page.
Dynamic select using dynamic constraints


Added by Steven Grimm, last edited by Steven Grimm on Aug 31, 2006  (view change)
Labels: 
(None)

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:

MapResourceBundle.java
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:

// Populate a map of category IDs to names. This assumes we already
// have pulled a list of categories from the database.
Map<Integer, String> categoryMap = new HashMap<Integer, String>();
for (Category cat : categoryList) {
	categoryMap.put(cat.getId(), cat.getName());
}

// "item" is our bean with the categoryId property. First we constrain its
// category ID to one of the legal values.
((Constrained) item).addConstraint(new ConstrainedProperty("categoryId")
						.inList(categoryMap.keySet()));

// Now we give the template the list of names for those category IDs. The
// second argument to the MapResourceBundle constructor will prefix all the
// keys with "categoryId:" so that RIFE knows to match them with the
// "categoryId" property of the bean.
template.addResourceBundle(new MapResourceBundle(categoryMap, "categoryId"));

// And finally, we tell RIFE we're done setting up the form and that it should
// use our bean instance to generate the HTML. We need to give it a bean instance
// even for an empty form because the constraints we just added need to be
// added to an actual instance (they could differ between invocations of the
// element, e.g. based on user permissions.)
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.)

SelectListHelper.java
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 &lt;select&gt; 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 &lt;select&gt; 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 we want a custom header, add it to the list first. The tricky bit
		// here is that we have to use the same object type as the rest of the
		// items in the list.
		if (firstItem != null) {
			if (selectItems.size() == 0) {
				firstItemObj = "dummy";
			}
			else {
				// Create an object that isn't already in the map but is the
				// same type as the existing keys in the map.
				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);
		}
		
		// Now add the list of valid values to the object.
		((Constrained)bean).addConstraint(new ConstrainedProperty(property)
								.inList(entries)
								.notEqual(firstItemObj));
		
		// Supply the map of values to descriptions to the template.
		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:

// Populate a map of category IDs to names. This assumes we already
// have pulled a list of categories from the database.
Map<Integer, String> categoryMap = new HashMap<Integer, String>();
for (Category cat : categoryList) {
	categoryMap.put(cat.getId(), cat.getName());
}

// "item" is our bean with the categoryId property.
SelectListHelper.prepareSelectList(template, item, "categoryId", categoryMap,
				null, "Please select a category.");

generateForm(template, item);
print(template);



Are you enjoying Confluence? Please consider purchasing it today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.2.1a Build:#515 May 19, 2006) - Bug/feature request - Contact Administrators