Overview
Sometimes it is necessary to dynamically build a values finder based on some outside influence (sometimes another values finder). This is how to create a value finder that uses input from another source. I am going to use an example where we want to give a list of available mileages for a user to select from. We only want to show mileages to the user based on the date their travel arrangements are made during. If we don't limit in this way, the number of available mileages to choose from will be infinite and indistinguishable. What we need is a list of mileages based on a date determined by the trip information already in the form. This is how we do that.Steps
1. Create a JstlFunctions class with static methods
package org.kuali.kfs.module.tem.web;
import static java.lang.Class.forName;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
import org.kuali.rice.kns.lookup.keyvalues.KeyValuesFinder;
/**
* Full of static methods for JSTL function access.
*
*/
public final class JstlFunctions {
private static final String SETTING_PARAMS_PROLOG = "Setting params ";
private static final String PROPERTY_SETTING_EXC_PROLOG = "Could not set property ";
private static final String IN_PREPOSITION = " in ";
private static final String VALUES_FINDER_CLASS_EXC_PROLOG = "Could not find valuesFinder class ";
private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(JstlFunctions.class);
private JstlFunctions() {}
/**
* Returns a list of key/value pairs for displaying in an HTML option for a select list. This is a customized approach to retrieving
* key/value data from database based on criteria specified in the <code>params {@link Map}</code><br/>
* <br/>
* Here is an example of how the code is used from a JSP:<br/>
* <code>
* <jsp:useBean id="paramMap" class="java.util.HashMap"/>
<c:set target="${paramMap}" property="forAddedPerson" value="true" />
<kul:checkErrors keyMatch="${proposalPerson}.proposalPersonRoleId" auditMatch="${proposalPerson}.proposalPersonRoleId"/>
<c:set var="roleStyle" value=""/>
<c:if test="${hasErrors==true}">
<c:set var="roleStyle" value="background-color:#FFD5D5"/>
</c:if>
<html:select property="${proposalPerson}.proposalPersonRoleId" tabindex="0" style="${roleStyle}">
<c:forEach items="${krafn:getOptionList('org.kuali.kra.proposaldevelopment.lookup.keyvalue.ProposalPersonRoleValuesFinder', paramMap)}" var="option">
<c:choose>
<c:when test="${KualiForm.document.proposalPersons[personIndex].proposalPersonRoleId == option.key}">
<option value="${option.key}" selected>${option.label}</option>
</c:when>
<c:otherwise>
<option value="${option.key}">${option.label}</option>
</c:otherwise>
</c:choose>
</c:forEach>
</html:select>
</code>
*
*
* @param valuesFinderClassName
* @param params mapped parameters
* @return List of key values
*/
@SuppressWarnings("unchecked")
public static List getOptionList(String valuesFinderClassName, Map params) {
return setupValuesFinder(valuesFinderClassName, (Map<String, Object>) params).getKeyValues();
}
/**
* Initiates the values finder by its <code>valuesFinderClassName</code>. First locates the class in the class path. Then,
* creates an instance of it. A <code>{@link Map}</code> of key/values <code>{@link String}</code> instances a is used
* to set properties on the values finder instance. Uses the apache <code>{@link PropertyUtils}</code> class to set properties
* by the name of the key in the <code>{@link Map}</code>.<br/>
* <br/>
* Basically, a new values finder is created. the <code>params</code> parameter is a <code>{@link Map}</code> of arbitrary values
* mapped to properties of the values finder class.<br/>
* <br/>
* Since this is so flexible and the ambiguity of properties referenced in the <code>{@link Map}</code>, a number of exceptions are caught
* if a property cannot be set or if the values finder cannot be instantiated. All of these exceptions are handled within the method. None
* of these exceptions are thrown back.
*
*
* @param valuesFinderClassName
* @param params
* @return KeyValuesFinder
* @see PropertyUtils#setProperty(Object, String, Object)
*/
private static KeyValuesFinder setupValuesFinder(String valuesFinderClassName, Map<String, Object> params) {
KeyValuesFinder retval = getKeyFinder(valuesFinderClassName);
if(LOG.isDebugEnabled()) {
LOG.debug(SETTING_PARAMS_PROLOG + params);
}
addParametersToFinder(params, retval);
return retval;
}
private static void addParametersToFinder(Map<String, Object> params, KeyValuesFinder finder) {
if (finder != null && params != null) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
try {
BeanUtils.setProperty(finder, entry.getKey(), entry.getValue());
// setProperty(finder, entry.getKey(), entry.getValue());
} catch (Exception e) {
warn(PROPERTY_SETTING_EXC_PROLOG + entry.getKey(), e);
e.printStackTrace();
}
}
}
}
private static KeyValuesFinder getKeyFinder(String valuesFinderClassName) {
KeyValuesFinder retval = null;
try {
retval = (KeyValuesFinder) forName(valuesFinderClassName).newInstance();
} catch (ClassNotFoundException e) {
warnAboutValueFinderClassExceptions(valuesFinderClassName, e);
} catch (InstantiationException e) {
warnAboutValueFinderClassExceptions(valuesFinderClassName, e);
} catch (IllegalAccessException e) {
warnAboutValueFinderClassExceptions(valuesFinderClassName, e);
}
return retval;
}
private static void warnAboutValueFinderClassExceptions(String valuesFinderClassName, Exception e) {
warn(VALUES_FINDER_CLASS_EXC_PROLOG + valuesFinderClassName, e);
}
private static void warn(String message, Exception e) {
if (LOG.isWarnEnabled()) {
LOG.warn(new StringBuilder(message).append(IN_PREPOSITION).append(buildTraceMessage(e)));
}
}
/**
* Get the stack trace from a <code>{@link Throwable}</code> and create a log message from it for tracing purposes
*
* @param thrownObj
* @return String log message
*/
private static String buildTraceMessage(Throwable thrownObj) {
StackTraceElement stackTraceElement = thrownObj.getStackTrace()[0];
return new StringBuilder(stackTraceElement.getClassName())
.append("#")
.append(stackTraceElement.getMethodName())
.append(":")
.append(stackTraceElement.getLineNumber())
.append(" ")
.append(thrownObj.getClass().getSimpleName())
.append("\n")
.append(thrownObj.getMessage())
.toString();
}
}
getOptionsList is the only public method in the class.@SuppressWarnings("unchecked")
public static List getOptionList(String valuesFinderClassName, Map params) {
return setupValuesFinder(valuesFinderClassName, (Map<String, Object>) params).getKeyValues();
}
You can see it returns a List because this will eventually be used with a JSP or JSTL tag file which do not support parameterized types. You can see that parameters are passed in as a Map called params. This is used via the addParametersToFinder method here:private static void addParametersToFinder(Map<String, Object> params, KeyValuesFinder finder) {
if (finder != null && params != null) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
try {
BeanUtils.setProperty(finder, entry.getKey(), entry.getValue());
// setProperty(finder, entry.getKey(), entry.getValue());
} catch (Exception e) {
warn(PROPERTY_SETTING_EXC_PROLOG + entry.getKey(), e);
e.printStackTrace();
}
}
}
}2. Create a JSTL TLD
Now that we have our class and our public method that will create our finder for us, we will need some way to invoke it. The best way to do this is by using the static method through a tag. We can create this tag easily with a TLD.<?xml version="1.0" encoding="UTF-8" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
<description>TEM functions library</description>
<display-name>TEM functions</display-name>
<tlib-version>1.0</tlib-version>
<short-name>fn</short-name>
<uri>http://www.kuali.org/jsp/jstl/functions</uri>
<function>
<description>Parameterized Values Finder List!!</description>
<name>getOptionList</name>
<function-class>org.kuali.kfs.module.tem.web.JstlFunctions</function-class>
<function-signature>java.util.List getOptionList(java.lang.String, java.util.Map)</function-signature>
<example><c:forEach items="${tem-fn:getOptionList()}">></example>
</function>
</taglib>
3. Use it in a JSP
In our JSP, we will need to define a Map to add parameters to. You saw earlier that our getOptionsList method takes a Map with parameters in it.<jsp:useBean id="paramMap" class="java.util.HashMap" />Then, we need to assign some value to the Map:
<c:set target="${paramMap}" property="queryDate"
value="${perDiemExpense.mileageDate}" />
Above we have assigned ${perDiemExpense.mileageDate} to the queryDate parameter in our Map! Now we just need our dropdown. <c:forEach items="${temfunc:getOptionList('org.kuali.kfs.module.tem.businessobject.options.MileageRateValuesFinder', paramMap)}" var="option">
<c:set var="mileageSelected" value="" />
<c:if test="${option.key} == KualiForm.document.perDiemExpenses[perDiemIndex.count - 1].mileageRateId}">
<c:set var="mileageSelected" value="selected" />
</c:if>
<option value="${option.key}"${mileageSelected}>${option.label}</option>
</c:forEach>Now you can see where getOptionList is actually used. Now we have a dropdown list of mileage rates based on the date of the mileage since mileage rates can vary by date.
