Tuesday, May 20, 2008

AJAX + Spring + HTMLTagTemplate

     Let's assume that I have a form with two fields, one called "Select Field", the second is a dynamic field that changes when "Select Field" value changes. How do I do that? Most likely you will say AJAX. That's right. But what if the second field can not only be a text field, but a select, or even some customized field, say calendar, as well? That can be a lot of possibilities and hard to maintain. So, I say, why not use HTMLTagTemplate? And after different try outs, I come up with this sample:

FormField.java

package net.yw.html;

import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;

import net.yw.resource.ResourceLookup;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * @author KWang
 *
 */
public class FormField {
    protected static Logger log = Logger.getLogger(FormField.class);

    // property name of the Object to be parsed as JSONForm's formEntry
    protected String name;

    protected boolean required;
    protected String labelKey;
    protected ResourceLookup lookups;

    // which tag to be used for this field when displaying
    protected HTMLTagTemplate tag;
    // properties to be added to the tag
    protected Map<String, Object> properties;
    protected Object body;
    
    //javascript to call with static field
    protected String javascript;
    
    //options for select tag
    protected SortedMap<String, String> options;

    public FormField() {
        // default tag is text
        this.tag = FormTagTemplate.text;
        this.properties = new HashMap<String, Object>();
        properties.put("size", 29);
        this.body = "";
    }

    /**
     * @return the labelKey
     */
    public String getLabelKey() {
        return labelKey;
    }

    /**
     * @param labelKey
     *            the labelKey to set
     */
    public void setLabelKey(String labelKey) {
        this.labelKey = labelKey;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name
     *            the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the properties
     */
    public Map<String, Object> getProperties() {
        return properties;
    }

    /**
     * @param properties
     *            the properties to set
     */
    public void setProperties(Map<String, Object> properties) {
        for(Map.Entry<String, Object> entry:properties.entrySet())
            this.properties.put(entry.getKey().toLowerCase(), entry.getValue());
    }
    
    public void addProperty(String key, Object value){
        this.properties.put(key.toLowerCase(), value);
    }

    /**
     * @return the required
     */
    public boolean isRequired() {
        return required;
    }

    /**
     * @param required
     *            the required to set
     */
    public void setRequired(boolean required) {
        this.required = required;
    }

    /**
     * @return the tag
     */
    public HTMLTagTemplate getTag() {
        return tag;
    }

    /**
     * @param tag
     *            the tag to set
     */
    public void setTag(HTMLTagTemplate tag) {
        if(tag == null || this.tag.equals(tag))
            return;
        this.tag = tag;
        properties.clear();
    }
    
    public void setTag(String tagName) throws HTMLTagException {
        try {
            setTag(FormTagTemplate.valueOf(tagName));
        } catch (RuntimeException e) {
            try {
                setTag(CustomTagTemplate.valueOf(tagName));
            } catch (RuntimeException e1) {
                throw new HTMLTagException("No valid tag found for " + tagName);
            }
        }
    }

    /**
     * @return the lookups
     */
    public ResourceLookup getLookups() {
        return lookups;
    }

    /**
     * @param lookups
     *            the lookups to set
     */
    public void setLookups(ResourceLookup bundle) {
        this.lookups = bundle;
    }

    public String getFieldTag() throws HTMLTagException {
        log.debug("JSONFormField getFieldTag() called");
        if (tag != null) {
            // seq. here is important, id is used for Ajax to locate element
            if (properties.get("name") == null || StringUtils.isBlank(properties.get("name").toString()))
                properties.put("name", this.name);
            if (properties.get("id") == null || StringUtils.isBlank(properties.get("id").toString()))
                properties.put("id", this.name);
            return  tag.doStart(properties) + getBody() + tag.doEnd();
        }        
        return "";
    }

    public String getLabelTag() {
        log.debug("JSONFormField getLabelTag() called");
        String labelString = "";
        if (required)
            labelString = "<span style=\"color:#FF0000;\">*</span>\n";
        labelString += getLabelValue();
        return labelString;
    }

    /**
     * @param labelString
     * @return
     */
    public String getLabelValue() {
        if (StringUtils.isNotBlank(labelKey) && lookups != null) {
            String value = lookups.getValue(labelKey);
            return StringUtils.isBlank(value) ? labelKey : value;
        }
        return StringUtils.isBlank(labelKey) ? "":labelKey;
    }

    // provides an clone copy, user can make modification without changing the
    // original
    public FormField getClone() {
        try {
            return (FormField) BeanUtils.cloneBean(this);
        } catch (Exception e) {
            return null;
        }
    }

    public String toString() {
        String string = getLabelTag() == null ? "" : getLabelTag();
        try {
            string += getFieldTag() == null ? "" : getFieldTag();
        } catch (HTMLTagException e) {
            e.printStackTrace();
        }
        return string;
    }
    
    public void setValue(String value){
        value = StringUtils.isBlank(value) ? "":value;
        if(tag instanceof FormTagTemplate){
            try {
                switch((FormTagTemplate)tag){
                    case radio:
                        for(String key:properties.keySet()){
                            if(StringUtils.equalsIgnoreCase(key, "value")){
                                String compareValue = properties.get(key).toString();
                                properties.put("checked", Boolean.valueOf(StringUtils.equals(value, compareValue)));
                                break;
                            }
                        }
                        break;
                    case checkbox:
                        if(!properties.containsKey("value"))
                            properties.put("value", "on");
                        properties.put("checked", Boolean.valueOf(value));
                        break;
                    case textarea:
                    case div:
                        this.body = value;
                        break;
                    default:
                        properties.put("value", value);
                        break;
                }
            } catch (RuntimeException e) {
                this.properties.put("value", value);
            }
        }else
            this.properties.put("value", value);
    }

    /**
     * @return the body
     */
    protected Object getBody() {
        //getBody is called only by getFieldTag, at this point tag should be final
        if(this.lookups != null && this.tag.equals(FormTagTemplate.select)){
            String value = properties.get("value") == null ? "":properties.get("value").toString();
            boolean blank = false;
            if(properties.containsKey("blank") && properties.get("blank") instanceof Boolean)
                blank = (Boolean)properties.get("blank");
            
            StringBuffer sb = new StringBuffer();
            FormTagTemplate option = FormTagTemplate.option;
            Map<String, Object> optionProp = new HashMap<String, Object>();
            try {
                if(blank){
                    optionProp.put("value", "");
                    optionProp.put("label", "");
                    sb.append(option.doStart(optionProp) + option.doEnd());
                }
                if(this.options != null && !this.options.isEmpty()){
                    for (Map.Entry<String, String> entry : options.entrySet()){
                        optionProp.put("value", entry.getKey().toString());
                        optionProp.put("label", entry.getValue().toString());
                        if(StringUtils.isNotBlank(value))
                            optionProp.put("selected", StringUtils.equals(value, entry.getKey().toString()));
                        sb.append(option.doStart(optionProp) + optionProp.get("label") + option.doEnd());
                    }
                }
            } catch (HTMLTagException e) {
                log.error("option rendering throw exception", e);
            }
            return sb.toString();
        }else if(StringUtils.isNotBlank(this.javascript)){
            //java solution is always prefered first
            return "<script>document.write(" + this.javascript + "('" + this.body + "'));</script>";
        }
        return body;
    }

    /**
     * @return the javascript
     */
    public String getJavascript() {
        return javascript;
    }

    /**
     * @param javascript the javascript to set
     */
    public void setJavascript(String javascript) {
        this.javascript = javascript;
    }

    /**
     * @return the options
     */
    public SortedMap<String, String> getOptions() {
        return options;
    }

    /**
     * @param options the options to set
     */
    public void setOptions(SortedMap<String, String> options) {
        this.options = options;
    }
    
    public String getValue(){
        if(tag.equals(FormTagTemplate.div)){
            if(StringUtils.isNotBlank(String.valueOf(this.getBody())))
                return String.valueOf(this.getBody());
        }
        return "";
    }

}


EmployeeForm.java

package net.yw.form;

import java.io.Serializable;

/**
 * @author KWang
 *
 */
public class EmployeeForm implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String fieldName;

    private Profile profile;

    /**
     * @return the fieldName
     */
    public String getFieldName() {
        return fieldName;
    }

    /**
     * @param fieldName the fieldName to set
     */
    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }

    /**
     * @return the profile
     */
    public Profile getProfile() {
        return profile;
    }

    /**
     * @param profile the profile to set
     */
    public void setProfile(Profile profile) {
        this.profile = profile;
    }
}


EmployeeFormAction.java

package net.yw.ajax.action;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.beanutils.BeanUtils;

import net.sf.json.JSONObject;
import net.yw.form.EmployeeForm;
import net.yw.html.FormField;

/**
 * @author KWang
 *
 */
public class EmployeeFormAction {

    public String getField(HttpServletRequest request) throws Exception{
        JSONObject object = new JSONObject(false);
        EmployeeForm form = (EmployeeForm) request.getAttribute("form");
        String fieldName = request.getParameter("$fieldName");
        String value = BeanUtils.getProperty(form, fieldName);
        for(FormField field:form.getEmployeeFields()){
            if(field.getName().equals(fieldName)){
                field.setValue(value);
                object.put("dynamicLabel", field.getLabelTag());
                object.put("dynamicField", field.getFieldTag());
                break;
            }
        }
        return object.toString();
    }
}


employeeform.jsp

<form action="javascript:callAjax()" name="employee">
    <table>
        <tr>
            <td>Select Field:</td>
            <td>
                <tags:select property="fieldName" onchange="javascript:callAjax()">
                    <option value="profile.salutation" />
                    <option value="profile.firstName" />
                    <option value="profile.lastName" />
                    <option value="profile.jobTitle" />
                    <option value="profile.dob" />
                    <option value="profile.cell" />
                    <option value="profile.createdDate" />
                    <option value="profile.lastUpdatedOn" />
                </tags:select>
            </td>
        </tr>
        <tr>
            <td><div id="dynamicLabel"></div>:</td>
            <td><div id="dynamicField"></div></td>
        </tr>
    </table>
    <script>
        function callAjax(){
            var parameters = '$action=EmployeeFormAction&$method=getField&$fieldName=' + $F('fieldName');
            new Ajax.Request("/[appName]/AjaxServlet", {asynchronous: false, parameters: parameters, onSuccess: function(request, json){
                try{
                    json = request.responseText.evalJSON(true);
                }catch(e){
                    alert('evalJSON:' +  $H(e).collect(function(entry){return entry.key + " " + entry.value;}).join(" || "));
                }
                $H(json).each(function(field){
                    $(field.key).update(field.value);
                });
            }});
        }
    </script>
</form>


applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

<beans default-autowire="byName">
    <bean id="textField" class="net.yw.html.FormField" scope="prototype">
        <property name="properties">
            <map>
                <entry key="size" value="29" />
            </map>
        </property>
    </bean>
    <bean id="staticField" class="net.yw.html.FormField" scope="prototype">
        <property name="tag" ref="selectTag" />
    </bean>
    <bean id="selectField" class="net.yw.html.FormField" scope="prototype">
        <property name="tag" ref="divTag" />
    </bean>
    <bean id="calendarField" class="net.yw.html.FormField" scope="prototype">
        <property name="tag" ref="calendarField" />
    </bean>

    <bean id="employeeFields" class="java.util.ArrayList">
        <constructor-arg>
            <list value-type="net.yw.html.FormField">
                <bean parent="selectField">
                    <property name="name" value="profile.salutation" />
                    <property name="labelKey" value="label.salutation" />
                    <property name="properties">
                        <map>
                            <entry key="blank">
                                <value type="java.lang.Boolean">true</value>
                            </entry>
                        </map>
                    </property>
                    <property name="options">
                        <map>
                            <entry key="MR" value="Mr." />
                            <entry key="MRS" value="Mrs." />
                            <entry key="MISS" value="Miss" />
                            <entry key="MS" value="Ms." />
                            <entry key="DR" value="Dr." />
                        </map>
                    </property>
                </bean>
                <bean parent="textField">
                    <property name="name" value="profile.firstName" />
                    <property name="labelKey" value="label.firstName" />
                    <property name="required" value="true" />
                </bean>
                <bean parent="textField">
                    <property name="name" value="profile.lastName" />
                    <property name="labelKey" value="label.lastName" />
                    <property name="required" value="true" />
                </bean>
                <bean parent="textField">
                    <property name="name" value="profile.jobTitle" />
                    <property name="labelKey" value="label.jobTitle" />
                    <property name="options">
                        <map>
                            <entry key="CLERK" value="Clerk" />
                            <entry key="SALESMAN" value="Salesman" />
                            <entry key="INSPECTOR" value="Inspector" />
                            <entry key="CONTRACTOR" value="Contractor" />
                            <entry key="ADMIN" value="Administrator" />
                        </map>
                    </property>
                </bean>
                <bean parent="calendarField">
                    <property name="name" value="profile.dob" />
                    <property name="labelKey" value="label.dob" />
                </bean>
                <bean parent="textField">
                    <property name="name" value="profile.cell" />
                    <property name="labelKey" value="label.cell" />
                </bean>
                <bean parent="staticField">
                    <property name="name" value="profile.createdDate" />
                    <property name="labelKey" value="label.createdDate" />
                    <property name="javascript" value="formatDate" />
                </bean>
                <bean parent="staticField">
                    <property name="name" value="profile.lastUpdatedOn" />
                    <property name="labelKey" value="label.lastUpdatedOn" />
                    <property name="javascript" value="formatDate" />
                </bean>
            </list>
        </constructor-arg>
    </bean>

    <bean id="selectTag" class="net.yw.html.FormTagTemplate" factory-method="valueOf">
        <constructor-arg value="select" />
    </bean>
    <bean id="divTag" class="net.yw.html.FormTagTemplate" factory-method="valueOf">
        <constructor-arg value="div" />
    </bean>
    <bean id="calendarTag" class="net.yw.html.CustomTagTemplate" factory-method="valueOf">
        <constructor-arg value="calendar" />
    </bean>
    <bean id="EmployeeFormAction" class="net.yw.ajax.action.EmployeeFormAction" />
    <bean id="employee" class="net.yw.form.EmployeeForm" />
</beans>


     I used Spring Based Ajax Servlet explained in my previous post. For more detail on servlet configuration, please see here. Please note that these sample code are for demonstration here. They are simplified from actual working code but haven't been tested. Also, ResourceLookup is a customized Spring configured Resource Bundle that is not covered here.

No comments: