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.

Custom Tag Using Tag File

     In my last post, I demonstrated use of EnumMap by creating two custom tags, calendar and number, implementing HTMLTagTemplate interface. However, you can also write the tag in pure tag file as following.

calendar.tag

<%@ attribute name="value" required="true" rtexprvalue="true" %>
<%@ attribute name="property" required="true" rtexprvalue="true" %>
<%@ attribute name="src" required="false" rtexprvalue="true" %>
<%@ attribute name="size" required="false" rtexprvalue="true" %>
<%@ attribute name="imgStyleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="styleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="errorStyleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="tabindex" required="false" rtexprvalue="true" %>
<%@ attribute name="timepicker" required="false" rtexprvalue="true" %>
<%@ attribute name="interval" required="false" rtexprvalue="true" %>

<%@ include file="/WEB-INF/jsp/common/includes.jsp"%>

<c:if test="${timepicker == null}"><c:set var="timepicker" value="false"/></c:if>
<c:if test="${interval == null}"><c:set var="interval" value="15"/></c:if>
<c:set var="inputName" value="${fn:replace(fn:replace(fn:replace(fn:replace(fn:replace(property, '.', ''), '(', ''), ')', ''), '[', ''), ']', '')}"/>
<c:set var="class" value="${styleClass == null ? 'right-align' : styleClass}"/>
<c:if test="${not empty request.errors.messages[property]}">
<c:set var="class" value="${errorStyleClass == null ? 'errorField' : errorStyleClass}"/>
</c:if> 
<input type="text" id="${inputName}" name="${inputName}" class="${class}" size="${size == null ? '29' : size}" tabindex="${tabindex}"/>
<img src="${src == null ? '../images/calendar.gif' : src}" id="${inputName}_img" name="${inputName}_img" class="${imgStyleClass == null ? 'calendar' : imgStyleClass}"/>
<div id="${inputName}_fmt"></div>
<input type="hidden" name="${property}" id="${property}" value="${value}"/>
<script type="text/javascript">
epochCalendar('${property}', '${inputName}', ${timepicker}, ${interval});
</script>


number.tag

<%@ attribute name="property" required="true" rtexprvalue="true" %>
<%@ attribute name="type" required="true" rtexprvalue="true" %>
<%@ attribute name="size" required="false" rtexprvalue="true" %>
<%@ attribute name="styleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="errorStyleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="tabindex" required="false" rtexprvalue="true" %>

<%@ include file="/WEB-INF/jsp/common/includes.jsp"%>

<c:set var="inputName" value="${fn:replace(property, '.', '')}"/>
<c:set var="class" value="${styleClass == null ? 'right-align' : styleClass}"/>
<c:if test="${not empty request.errors.messages[property]}">
<c:set var="class" value="${errorStyleClass == null ? 'errorField' : errorStyleClass}"/>
</c:if> 
<input type="text" id="${inputName}" name="${inputName}" class="${class}" size="${size == null ? "29" : size}" tabindex="${tabindex}"
onblur="javascript:formatNumberFunc(this,'${property}','${type}');"
onfocus="javascript:unformatNumber(this,'${type}');" maxlength="11"/>
<tags:input type="hidden" property="${property}" />
<script type="text/javascript">
if($('${property}').value == null || $('${property}').value == '')
$('${property}').value = 0;
$('${inputName}').value = formatNumberFunction($('${property}').value,'${type}');
</script>


     Please note that these tag files uses prototype in javascript. Following is some sample code in JSP page.


<%-- calendar selector for date of birth --%>
<tags:calendar value="${request.form.dob}" property="dob" tabindex="1"/>

<%-- format a number in currency --%>
<tags:number property="retailPrice" type="currency" tabindex="2"/>

Use of EnumMap


     I have posted a few blogs explaining my idea of using Enum to create FormTagTemplate class which writes HTML tags. In this post, I'll use FormTagTemplate in CustomTagTemplate to sample the use of EnumMap.

     CustomTagTemplate has two custom tags: calendar and number. Calendar tag, as you can see from the image on the left, has one text field, one hidden field, and one image. When you click on the image or select the text field, a calendar will pop up. Number tag has one text field and one hidden field. It formats the number entered when the text field is onblur and unformat the number when the text field is onfocus. The actual raw value, however, is stored in the hidden field for both tags. Both tags require javascripts which I do not cover here. Calendar tag's javascript is my personal modification from Epoch JavaScript Calendar. I also added on a time picker with customizable time interval.

CustomTagTemplate.java

package net.yw.html;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;


/**
 * @author KWang
 *
 */
public enum CustomTagTemplate implements HTMLTagTemplate {
    calendar() {
        protected void setDefaults(){
            Map<String, Object> properties = new HashMap<String, Object>();
            properties.put("class", "right-align");
            properties.put("size", 29);
            this.defaults.put(FormTagTemplate.text, properties);
            
            properties = new HashMap<String, Object>();
            properties.put("src", "../images/calendar.gif");
            properties.put("class", "calendar");
            properties.put("alt", "");
            this.defaults.put(FormTagTemplate.img, properties);
        }
        
        public String doEnd() {
            return "";
        }

        public String doStart(Map<String, Object> propertyMap) throws HTMLTagException {
            StringBuffer sb = new StringBuffer();
            String name = propertyMap.get("name").toString();
            
            Map<String, Object> defaults = this.defaults.get(FormTagTemplate.text);
            defaults.putAll(propertyMap);
            if(defaults.containsKey("value"))
                defaults.remove("value");
            defaults.put("name", name + "_input");
            defaults.put("id", name + "_input");
            sb.append(FormTagTemplate.text.doStart(defaults));
            
            sb.append("\n");
            defaults = this.defaults.get(FormTagTemplate.img);
            defaults.put("name", name + "_input_img");
            defaults.put("id", name + "_input_img");
            sb.append(FormTagTemplate.img.doStart(defaults));
            
            sb.append("\n");
            defaults = new HashMap<String, Object>();
            defaults.put("id", name + "_input_fmt");
            sb.append(FormTagTemplate.div.doStart(defaults) + FormTagTemplate.div.doEnd());
            
            sb.append("\n");
            defaults = new HashMap<String, Object>();
            defaults.put("name", name);
            defaults.put("id", name);
            if(propertyMap.containsKey("value"))
                defaults.put("value", propertyMap.get("value"));
            sb.append(FormTagTemplate.hidden.doStart(defaults));
            
            boolean timepicker = propertyMap.containsKey("timepicker") ? Boolean.valueOf(propertyMap.get("timepicker").toString()) : false;
            int interval = propertyMap.containsKey("interval") ? Integer.valueOf(propertyMap.get("interval").toString()) : 15;
            sb.append("<script type=\"text/javascript\">");
            sb.append("epochCalendar('" + name + "', '" + name + "_input', " + timepicker + ", " + interval + ");");
            sb.append("</script>");
            return sb.toString();
        }
    },
    
    number(){
        protected void setDefaults(){
            Map<String, Object> properties = new HashMap<String, Object>();
            properties.put("class", "right-align");
            properties.put("size", 29);
            properties.put("maxlength", 11);
            this.defaults.put(FormTagTemplate.text, properties);
        }
        
        public String doEnd() {
            return "";
        }

        public String doStart(Map<String, Object> propertyMap) throws HTMLTagException {
            StringBuffer sb = new StringBuffer();
            String name = propertyMap.get("name").toString();
            String format = propertyMap.get("format").toString();
            
            Map<String, Object> defaults = this.defaults.get(FormTagTemplate.text);
            defaults.putAll(propertyMap);
            if(defaults.containsKey("value"))
                defaults.remove("value");
            defaults.put("name", name + "_input");
            defaults.put("id", name + "_input");
            defaults.put("onblur", "javascript:formatNumberFunc(this,'" + name + "','" + format + "');");
            defaults.put("onfocus", "javascript:unformatNumber(this,'" + format + "');");
            sb.append(FormTagTemplate.text.doStart(defaults));
            
            sb.append("\n");
            defaults = new HashMap<String, Object>();
            defaults.put("name", name);
            defaults.put("id", name);
            Object value = "0";
            if(propertyMap.containsKey("value"))
                value = propertyMap.get("value");
            if(value == null || StringUtils.isBlank(value.toString()) || !StringUtils.isNumeric(value.toString()))
                value = "0";
            defaults.put("value", value);
            sb.append(FormTagTemplate.hidden.doStart(defaults));
            
            sb.append("\n");
            sb.append("<script type=\"text/javascript\">");
            sb.append("$('" + name + "_input').value = formatNumberFunction(" + value + ", " + format + ");");
            sb.append("</script>");
            return sb.toString();
        }
    };
    
    protected EnumMap<FormTagTemplate, Map<String, Object>> defaults = new EnumMap<FormTagTemplate, Map<String, Object>>(FormTagTemplate.class);
    
    private CustomTagTemplate(){
        this.setDefaults();
    }
    
    protected abstract void setDefaults();

    public abstract String doEnd();

    public abstract String doStart(Map<String, Object> propertyMap) throws HTMLTagException;
}


     Of course you can add a calendar and a number function to the Utility class and create tag files in the same way as I did for input and select tag. However, it will be much easier if you just write it in pure tag file. Then you might wonder why go through all the trouble to write CustomTagTemplate? The answer is to use it for AJAX calls, which will be covered soon.

HTML Tag Template - Tag File

     Now that we've done the Java part, let's get down to Tag File. First of all, we need to add value, input, and select functions from the Utility class to a tld file and place it under WEB-INF.

html.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/web-jsptaglibrary_2_0.xsd" version="2.0">
<tlib-version>1.0</tlib-version>
<short-name>HTMLFunc</short-name>
<function>
<name>value</name>
<function-class>net.yw.html.HTMLTagUtils</function-class>
<function-signature>java.lang.String value(java.lang.Object, java.lang.String, boolean)</function-signature>
</function>
<function>
<name>input</name>
<function-class>net.yw.html.HTMLTagUtils</function-class>
<function-signature>java.lang.String input(java.lang.Object, java.lang.String, java.lang.String, java.lang.String, java.util.Map)</function-signature>
</function>
<function>
<name>select</name>
<function-class>net.yw.html.HTMLTagUtils</function-class>
<function-signature>java.lang.String select(java.lang.Object, java.lang.String, java.lang.String, java.util.Map, java.lang.Object, java.lang.String, java.lang.String, java.lang.String)</function-signature>
</function>
</taglib>


     Also, you'll need to create a tags folder under WEB-INF which will holds all the tag files. In addition, an include.jsp which holds all the tag definition is imported in each tag file.

includes.jsp

<%--  To use Servlet 2.4, the following setting should have a "/jsp" before "/jstl" --%>
<%-- JSTL Tags --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn"  uri="http://java.sun.com/jsp/jstl/functions" %>

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<%-- Other Tags --%>
<%@ taglib uri="/WEB-INF/html.tld" prefix="html" %>
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags"%>


     Assume that form and error information is hold in request, and a default error style -- 'errorField' exists, we now have two standard tag files.

input.tag

<%@ tag dynamic-attributes="attrMap" %>
<%@ attribute name="type" required="true" rtexprvalue="true" %>
<%@ attribute name="property" required="true" rtexprvalue="true" %>
<%@ attribute name="styleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="errorStyleClass" required="false" rtexprvalue="true" %>

<%@ include file="/WEB-INF/jsp/common/includes.jsp"%>

<c:set var="type" value="${fn:toLowerCase(type)}"/>
<c:set var="class" value="${styleClass == null ? 'right-align' : styleClass}"/>
<c:if test="${not empty request.errors.messages[property]}">
<c:set var="class" value="${errorStyleClass == null ? 'errorField' : errorStyleClass}"/>
</c:if>
${html:input(request.form, property, class, type, attrMap)}


select.tag

<%@ tag dynamic-attributes="attrMap" %>
<%@ attribute name="property" required="true" rtexprvalue="true" %>
<%@ attribute name="collections" required="false" rtexprvalue="true" type="java.lang.Object"%>
<%@ attribute name="valueProperty" required="false" rtexprvalue="true" %>
<%@ attribute name="labelProperty" required="false" rtexprvalue="true" %>
<%@ attribute name="styleClass" required="false" rtexprvalue="true" %>
<%@ attribute name="errorStyleClass" required="false" rtexprvalue="true" %>

<%@ include file="/WEB-INF/jsp/common/includes.jsp"%>

<c:set var="class" value="${styleClass == null ? '' : styleClass}"/>
<c:if test="${not empty request.errors.messages[property]}">
<c:set var="class" value="${errorStyleClass == null ? 'errorField' : errorStyleClass}"/>
</c:if>
<c:set var="optionString"><jsp:doBody /></c:set>
${html:select(request.form, property, class, attrMap, collections, valueProperty, labelProperty, optionString)}


     Note there is an attribute called attrMap which is a dynamic-attribute. With attrMap in place, you can add any extra attribute to the tag besides what have been defined in the tag file. And these extra attributes will be passed as the HTML Tag's atrribute values. It is safe to do so as long as the property filter process in FormTagTemplate.java is in place.

     For select tag, the user has multiple ways to specify options. You can write the options manually between the select tag. Or, you can pass in a collection or an array and specify the value and label property. Also, you can pass in a map which's key is the value property and value is the label property. There is also a blank option indicating adding a blank option on top of the option list when it's true.

     Here is some example:

<tags:input type="text" property="stockNumber" size="29" styleClass="left-align" tabindex="1" errorStyleClass="errorClass" maxlength="20"/>

<tags:select property="typeId" collections="${request.form.optionMap}" blank="true" tabindex="2" />

<tags:select property="selectedName" style="width: 80px" onchange="javascript:doSubmit();">
    <c:forEach var="name" items="${request.form.availableNames}">
        <c:choose>
            <c:when test="${name[0] == request.form.selectedName}">
                <option value="${name[0]}" selected>
            </c:when>
            <c:otherwise>
                <option value="${name[0]}">
            </c:otherwise>
        </c:choose>
        <c:catch var="error">
            <tags:label key="${name[1]}" />
        </c:catch>
        </option>
    </c:forEach>
</tags:select>

<%-- request.form.styles is an array --%>
<tags:select property="style" collections="${request.form.styles}" valueProperty="0" labelProperty="1" tabindex="4" blank="false" style="width:178px" />


     Coming up next, more on custom HTML Tag and use of HTMLTagTemplate in AJAX.

HTML Tag Template - Utility

     In my previous post, I wrote a FormTagTemplate which can write form tags. However, since I don't use TagSupport, I'll need a little utility class in order to actually use it to write tags in jsp files.

HTMLTagUtils.java

package net.yw.html;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

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

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

    // ResponseUtils.filter(String)
    public static String filter(String value) {
        if (value == null || value.length() == 0)
            return value;
        StringBuffer result = null;
        String filtered = null;
        for (int i = 0; i < value.length(); i++) {
            filtered = null;
            switch (value.charAt(i)) {
                case '<':
                    filtered = "&lt;";
                    break;
                case '>':
                    filtered = "&gt;";
                    break;
                case '&':
                    filtered = "&amp;";
                    break;
                case '"':
                    filtered = "&quot;";
                    break;
                case '\'':
                    filtered = "&#39;";
                    break;
            }

            if (result == null) {
                if (filtered != null) {
                    result = new StringBuffer(value.length() + 50);
                    if (i > 0) {
                        result.append(value.substring(0, i));
                    }
                    result.append(filtered);
                }
            } else {
                if (filtered == null) {
                    result.append(value.charAt(i));
                } else {
                    result.append(filtered);
                }
            }
        }

        return result == null ? value : result.toString();
    }

    public static String value(Object bean, String property, boolean escapeXml) throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        String value = BeanUtils.getProperty(bean, property);
        if(value == null)
            return "";
        return escapeXml?filter(value):value;
    }

    @SuppressWarnings("unchecked")
    public static String select(Object bean, String property, String styleClass, Map<String, Object> properties,
            Object collections, String valueProperty, String labelProperty, String optionString) throws  HTMLTagException {
        if(StringUtils.isBlank(property))
            throw new HTMLTagException("property is missing");
        FormTagTemplate select = FormTagTemplate.select;
        properties.put("name", property);
        if(!properties.containsKey("id"))
            properties.put("id", properties.get("name"));
        if(StringUtils.isNotBlank(styleClass))
            properties.put("class", styleClass);
        
        StringBuffer sb = new StringBuffer(select.doStart(properties));
        
        //options
        FormTagTemplate option = FormTagTemplate.option;
        if(properties.containsKey("blank")){
            boolean blank = Boolean.parseBoolean(properties.get("blank").toString());
            if(blank){
                Map<String, Object> blankOp = new HashMap<String, Object>();
                blankOp.put("value", "");
                blankOp.put("label", "");
                sb.append(option.doStart(blankOp) + option.doEnd());
            }
        }
        if(collections != null){
            try {
                String value = bean == null ? "":value(bean, property, false);
                Map<String, Object> optionProp = new HashMap<String, Object>();
                if(collections instanceof Map){
                    for(Map.Entry<Object, Object> entry:((Map<Object, Object>)collections).entrySet()){
                        optionProp.put("value", entry.getKey().toString());
                        optionProp.put("label", entry.getValue().toString());
                        optionProp.put("selected", StringUtils.equals(value, entry.getKey().toString()));
                        sb.append(option.doStart(optionProp) + optionProp.get("label") + option.doEnd());
                    }
                }else if(collections instanceof Collection){
                    for(Object object:((Collection)collections)){
                        optionProp.put("value", StringUtils.isBlank(valueProperty)?object.toString():value(object, valueProperty, false));
                        optionProp.put("label", StringUtils.isBlank(labelProperty)?object.toString():value(object, labelProperty, false));
                        optionProp.put("selected", StringUtils.equals(value, optionProp.get("value").toString()));
                        sb.append(option.doStart(optionProp) + optionProp.get("label") + option.doEnd());
                    }
                }else if(collections.getClass().isArray()){
                    for(Object object:Arrays.asList((Object[])collections)){
                        if(object.getClass().isArray()){
                            int valueIndex = StringUtils.isNumeric(valueProperty)?Integer.valueOf(valueProperty):0;
                            int labelIndex = StringUtils.isNumeric(labelProperty)?Integer.valueOf(labelProperty):0;
                            optionProp.put("value", ((Object[])object)[valueIndex]);
                            optionProp.put("label", ((Object[])object)[labelIndex]);
                            optionProp.put("selected", StringUtils.equals(value, optionProp.get("value").toString()));
                            sb.append(option.doStart(optionProp) + optionProp.get("label") + option.doEnd());
                        }else{
                            optionProp.put("value", StringUtils.isBlank(valueProperty)?object.toString():value(object, valueProperty, false));
                            optionProp.put("label", StringUtils.isBlank(labelProperty)?object.toString():value(object, labelProperty, false));
                            optionProp.put("selected", StringUtils.equals(value, optionProp.get("value").toString()));
                            sb.append(option.doStart(optionProp) + optionProp.get("label") + option.doEnd());
                        }
                    }
                }
            } catch (IllegalAccessException e) {
                throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
            } catch (InvocationTargetException e) {
                throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
            } catch (NoSuchMethodException e) {
                throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
            }
        }
        
        if(StringUtils.isNotBlank(optionString))
            sb.append(optionString);
        sb.append(select.doEnd());
        return sb.toString();
    }
    
    public static String input(Object bean, String property, String styleClass, String type, Map<String, Object> properties) throws HTMLTagException{
        if(StringUtils.isBlank(property))
            throw new HTMLTagException("property is missing");
        FormTagTemplate tag = FormTagTemplate.valueOf(type);
        if(tag == null)
            throw new HTMLTagException("tag " + type + " not found!");
        else if(tag.equals(FormTagTemplate.select) || tag.equals(FormTagTemplate.option))
            throw new HTMLTagException("Please use select for select/option type tag!");
        else if(tag.equals(FormTagTemplate.div))
            throw new HTMLTagException("Please use div tag directly in page!");

        properties.put("name", property);
        if(!properties.containsKey("id"))
            properties.put("id", properties.get("name"));
        if(StringUtils.isNotBlank(styleClass))
            properties.put("class", styleClass);

        try {
            String value = bean == null || tag.equals(FormTagTemplate.file) ? "":value(bean, property, false);
            switch(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;
                default:
                    properties.put("value", value);
                    break;
            }
            
            StringBuffer sb = new StringBuffer(tag.doStart(properties));
            if(tag.equals(FormTagTemplate.textarea) || tag.equals(FormTagTemplate.div))
                sb.append(value + tag.doEnd());
            return sb.toString();
        } catch (IllegalAccessException e) {
            throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
        } catch (InvocationTargetException e) {
            throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
        } catch (NoSuchMethodException e) {
            throw new HTMLTagException("Cannot get value of " + property + " from " + bean );
        }
    }
}


     The utility only has four functions. filter() is identical to ResponseUtils.filter() in Struts. value() retrieves the String value of a property in a bean. select() and input() is used in select and input tag files in my next post.