Kualigan

Blog of a Kuali Foundation Release Manager. Full of code examples, solutions, best practices, et al.


Wednesday, December 21, 2011

Kuali Parameterized ValueFinder Implementation

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.

Thursday, December 1, 2011

SEVERE: Error listenerStart

Overview

I saw on the rice mailinglist recently someone posted a question where essentially, the application dies before a logger can be initialized. Here is the result:
INFO: Deploying web application archive kr-dev.war
log4j:WARN No appenders could be found for logger (org.kuali.rice.core.web.listener.KualiInitializeListener).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Dec 1, 2011 1:35:19 PM org.apache.catalina.core.StandardContext start
SEVERE: Error listenerStart
Dec 1, 2011 1:35:19 PM org.apache.catalina.core.StandardContext start
SEVERE: Context [/kr-dev] startup failed due to previous errors

What has happened here is the listener failed to start. The listener is what starts up the application (in this case, Rice). If Rice does not start, there is no logger to tell you what is wrong. Thus, you get no error. Usually, what causes this is a dependency is not included in tomcat.

So far, I have not seen a case where the problem is caused by anything but a missing library. It is usually the database driver, because that is the only thing not included in the rice war by default. It can possibly be something else though. I have seen KFS not start because of a missing log4j file. Since KFS actually requires the application to deploy additional libraries to the application server before startup, it is the only exceptional case.

Solution

Of course, the solution is to make sure your database driver is in the tomcat/lib folder, right? What if that doesn't do the job? What if it's another library? Read on.

Alternate

Sometimes you need to figure out what library is missing. This is almost never the case. Nearly always it is the database driver that is missing. Almost, right? If this happens and the database driver is loading correctly, then you need to figure out what the problem is. This means consulting the tomcat logger. I am going to assume that you have installed the rice war file in your tomcat installation. The war file is now unpacked and there's a webapp directory somewhere. Within the WEB-INF/classes path of that webapp directory, you can create a logging.properties with the following contents.
org.apache.catalina.core.ContainerBase.[Catalina].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].handlers = java.util.logging.ConsoleHandler
Why? The reason is that rice uses commons-logging and log4j while tomcat uses java.util.logging. Essentially, separate things. Also, since Kuali did not start, it did not start the logging configuration. What we can do is add a logging configuration that tomcat will pick up and use when it tries to start Rice. That is essentially what we are doing here.

Now try to restart the server. You should see some kind of NoClassDefFoundError somewhere explaining what library you are missing.

Tuesday, November 15, 2011

Getting Started with Rice 2.0

This is my "Getting Started with Rice 2.0" presentation

Enjoy

Wednesday, November 9, 2011

Current KIM Refactoring Documentation

Overview

There are 2 ways to add objects to KIM.
  • Rice User Interface
  • SQL insert statements when building the application
. Since Kuali is venturing into the use of liquibase, I have put together custom refactorings that simplify adding objects to KIM and other Rice modules.

Using SQL

This is how one would normally create an attribute, create a responsibility, add the responsibility to a role, and add an attribute to a responsibility.

Example

-- International Travel Reviewer Node
insert into krim_rsp_t (rsp_id, nmspc_cd, nm, actv_ind, rsp_tmpl_id, ver_nbr, obj_id)
values(krim_rsp_id_s.nextval, 'KFS-TEM', 'Review', 'Y', 1, 1, sys_guid());


insert into krim_role_t (role_id, obj_id, ver_nbr, nmspc_cd, role_nm, kim_typ_id, actv_ind, last_updt_dt)
   values (krim_role_id_s.nextval, sys_guid(), 1, 'KFS-TEM', 'International Travel Reviewer',
   (select kim_typ_id from krim_typ_t where nm = 'Default' and nmspc_cd = 'KUALI' and actv_ind = 'Y'),
    'Y', SYSDATE);

insert into krim_rsp_attr_data_t (attr_data_id, obj_id, ver_nbr, rsp_id, kim_typ_id, kim_attr_defn_id, attr_val)
values(krim_rsp_rqrd_attr_id_s.nextval, sys_guid(), 1, krim_rsp_id_s.currval, 7, 16, 'InternationalTravelReviewer');

insert into krim_rsp_attr_data_t (attr_data_id, obj_id, ver_nbr, rsp_id, kim_typ_id, kim_attr_defn_id, attr_val)
values(krim_rsp_rqrd_attr_id_s.nextval, sys_guid(), 1, krim_rsp_id_s.currval, 7, 13, 'TA');

insert into krim_rsp_attr_data_t (attr_data_id, obj_id, ver_nbr, rsp_id, kim_typ_id, kim_attr_defn_id, attr_val)
values(krim_rsp_rqrd_attr_id_s.nextval, sys_guid(), 1, krim_rsp_id_s.currval, 7, 41, 'false');

insert into krim_rsp_attr_data_t (attr_data_id, obj_id, ver_nbr, rsp_id, kim_typ_id, kim_attr_defn_id, attr_val)
values(krim_rsp_rqrd_attr_id_s.nextval, sys_guid(), 1, krim_rsp_id_s.currval, 7, 40, 'false');

insert into krim_role_rsp_t (role_rsp_id, obj_id, ver_nbr, role_id, rsp_id, actv_ind)
values (krim_role_rsp_id_s.nextval, sys_guid(), 1, (select role_id from krim_role_t where role_nm = 'International Travel Reviewer' and nmspc_cd= 'KFS-TEM'), krim_rsp_id_s.currval, 'Y');

insert into krim_role_rsp_actn_t (role_rsp_actn_id, obj_id, ver_nbr, actn_typ_cd, priority_nbr, actn_plcy_cd, role_mbr_id, role_rsp_id, frc_actn)
   values (krim_role_rsp_actn_id_s.nextval, sys_guid(), 1, 'A', 1, 'F', '*',
   (select role_rsp_id from krim_role_rsp_t where role_id = (select role_id from krim_role_t where role_nm = 'International Travel Reviewer' and nmspc_cd = 'KFS-TEM')),
    'N');

New Liquibase Refactoring Method

I have documented a better way on the Rice Liquibase Extensions page.


Tuesday, November 8, 2011

Project Updates and the Coming Onslaught of Documentation

I have been working on improving the documentation in the projects surrounding liquibase. That's right. The changes aren't all just for looks. A lot of documentation that is going out to wikis will be updated on the official project pages first. Also, among the documentation improvements are:
  • javadoc APIs
  • Usage instructions
  • Screencasts (most importantly is that they're better and more informative screencasts)
  • Working example (examples taken directly from implemented scenarios

Updated Projects

Here is a list of projects recently updated:

rice-lb-extensions

This is a new project that is going to focus on custom refactorings. Instead of having a bunch of SQL inserts, database specific sql, and having to keep track of your previous ID, there will be meaningful code behind your database object changes. Here are some up and coming KIM Refactorings.

Monday, November 7, 2011

New GitHub Project

Overview

I've created a new project on GitHub. It is called project. What it is, is a maven skin that will make your maven site look just like the Kuali site.

Sites Used

I am currently using this skin for my rsmart-lb-ant project. Which brings me to my next update. I am currently working on improving my documentation on rsmart-lb-ant and rsmart-lb-extensions. I will be providing better examples and videos on how to use these too. Examples, are from real, working implementations.

I hope you enjoy it.

Wednesday, September 28, 2011

LDAP KIM Integration Documentation

I have added documentation in a permanent location that will be updated and maintained for KIM LDAP Integration