/**
* Copyright 2005-2010 hdiv.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hdiv.web.servlet.tags.form;
import java.util.StringTokenizer;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hdiv.dataComposer.IDataComposer;
import org.hdiv.util.HDIVUtil;
import org.hdiv.web.util.TagUtils;
import org.springframework.beans.PropertyAccessor;
import org.springframework.core.Conventions;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.tags.form.AbstractFormTag;
import org.springframework.web.servlet.tags.form.AbstractHtmlElementTag;
import org.springframework.web.servlet.tags.form.TagWriter;
import org.springframework.web.util.HtmlUtils;
/**
* Databinding-aware JSP tag for rendering an HTML '<code>form</code>' whose
* inner elements are bound to properties on a
* {@link #setCommandName command object}.
*
* <p>
* Users should place the command object into the
* {@link org.springframework.web.servlet.ModelAndView} when populating the data
* for their view. The name of this command object can be configured using the
* {@link #setCommandName commandName} property.
*
* <p>
* The default value for the {@link #setCommandName commandName} property is '<code>command</code>'
* which corresponds to the default name when using the
* {@link org.springframework.web.servlet.mvc.SimpleFormController}.
*
* <p>
* Inner tags can access the name of the command object via the
* {@link javax.servlet.jsp.PageContext}. The attribute name is defined in
* {@link #COMMAND_NAME_VARIABLE_NAME}.
*
* @author Gorka Vicente
* @since HDIV 2.1.0
* @see org.springframework.web.servlet.tags.form.FormTag
*/
public class FormTagHDIV extends AbstractHtmlElementTag {
/**
* Commons Logging instance.
*/
private static Log log = LogFactory.getLog(FormTagHDIV.class);
/** The default HTTP method using which form values are sent to the server: "post" */
private static final String DEFAULT_METHOD = "post";
/** The default attribute name: "command" */
public static final String DEFAULT_COMMAND_NAME = "command";
/** The name of the '<code>modelAttribute</code>' setting */
private static final String MODEL_ATTRIBUTE = "modelAttribute";
/**
* The name of the {@link javax.servlet.jsp.PageContext} attribute under which the
* form object name is exposed.
*/
public static final String MODEL_ATTRIBUTE_VARIABLE_NAME =
Conventions.getQualifiedAttributeName(AbstractFormTag.class, MODEL_ATTRIBUTE);
/** Default method parameter, i.e. <code>_method</code>. */
private static final String DEFAULT_METHOD_PARAM = "_method";
private static final String FORM_TAG = "form";
private static final String INPUT_TAG = "input";
private static final String ACTION_ATTRIBUTE = "action";
private static final String METHOD_ATTRIBUTE = "method";
private static final String TARGET_ATTRIBUTE = "target";
private static final String ENCTYPE_ATTRIBUTE = "enctype";
private static final String ACCEPT_CHARSET_ATTRIBUTE = "accept-charset";
private static final String ONSUBMIT_ATTRIBUTE = "onsubmit";
private static final String ONRESET_ATTRIBUTE = "onreset";
private static final String AUTOCOMPLETE_ATTRIBUTE = "autocomplete";
private static final String NAME_ATTRIBUTE = "name";
private static final String VALUE_ATTRIBUTE = "value";
private static final String TYPE_ATTRIBUTE = "type";
private TagWriter tagWriter;
private String modelAttribute = DEFAULT_COMMAND_NAME;
private String name;
private String action;
private String method = DEFAULT_METHOD;
private String target;
private String enctype;
private String acceptCharset;
private String onsubmit;
private String onreset;
private String autocomplete;
private String methodParam = DEFAULT_METHOD_PARAM;
/** Caching a previous nested path, so that it may be reset */
private String previousNestedPath;
private IDataComposer dataComposer;
/**
* Set the name of the form attribute in the model.
* <p>May be a runtime expression.
*/
public void setModelAttribute(String modelAttribute) {
this.modelAttribute = modelAttribute;
}
/**
* Get the name of the form attribute in the model.
*/
protected String getModelAttribute() {
return this.modelAttribute;
}
/**
* Set the name of the form attribute in the model.
* <p>May be a runtime expression.
* @see #setModelAttribute
*/
public void setCommandName(String commandName) {
this.modelAttribute = commandName;
}
/**
* Get the name of the form attribute in the model.
* @see #getModelAttribute
*/
protected String getCommandName() {
return this.modelAttribute;
}
/**
* Set the value of the '<code>name</code>' attribute.
* <p>May be a runtime expression.
* <p>Name is not a valid attribute for form on XHTML 1.0. However,
* it is sometimes needed for backward compatibility.
*/
public void setName(String name) {
this.name = name;
}
/**
* Get the value of the '<code>name</code>' attribute.
*/
@Override
protected String getName() throws JspException {
return this.name;
}
/**
* Set the value of the '<code>action</code>' attribute.
* <p>May be a runtime expression.
*/
public void setAction(String action) {
this.action = (action != null ? action : "");
}
/**
* Get the value of the '<code>action</code>' attribute.
*/
protected String getAction() {
return this.action;
}
/**
* Set the value of the '<code>method</code>' attribute.
* <p>May be a runtime expression.
*/
public void setMethod(String method) {
this.method = method;
}
/**
* Get the value of the '<code>method</code>' attribute.
*/
protected String getMethod() {
return this.method;
}
/**
* Set the value of the '<code>target</code>' attribute.
* <p>May be a runtime expression.
*/
public void setTarget(String target) {
this.target = target;
}
/**
* Get the value of the '<code>target</code>' attribute.
*/
public String getTarget() {
return this.target;
}
/**
* Set the value of the '<code>enctype</code>' attribute.
* <p>May be a runtime expression.
*/
public void setEnctype(String enctype) {
this.enctype = enctype;
}
/**
* Get the value of the '<code>enctype</code>' attribute.
*/
protected String getEnctype() {
return this.enctype;
}
/**
* Set the value of the '<code>acceptCharset</code>' attribute.
* <p>May be a runtime expression.
*/
public void setAcceptCharset(String acceptCharset) {
this.acceptCharset = acceptCharset;
}
/**
* Get the value of the '<code>acceptCharset</code>' attribute.
*/
protected String getAcceptCharset() {
return this.acceptCharset;
}
/**
* Set the value of the '<code>onsubmit</code>' attribute.
* <p>May be a runtime expression.
*/
public void setOnsubmit(String onsubmit) {
this.onsubmit = onsubmit;
}
/**
* Get the value of the '<code>onsubmit</code>' attribute.
*/
protected String getOnsubmit() {
return this.onsubmit;
}
/**
* Set the value of the '<code>onreset</code>' attribute.
* <p>May be a runtime expression.
*/
public void setOnreset(String onreset) {
this.onreset = onreset;
}
/**
* Get the value of the '<code>onreset</code>' attribute.
*/
protected String getOnreset() {
return this.onreset;
}
/**
* Set the value of the '<code>autocomplete</code>' attribute.
* May be a runtime expression.
*/
public void setAutocomplete(String autocomplete) {
this.autocomplete = autocomplete;
}
/**
* Get the value of the '<code>autocomplete</code>' attribute.
*/
protected String getAutocomplete() {
return this.autocomplete;
}
/**
* Set the name of the request param for non-browser supported HTTP methods.
*/
public void setMethodParam(String methodParam) {
this.methodParam = methodParam;
}
/**
* Get the name of the request param for non-browser supported HTTP methods.
*/
protected String getMethodParameter() {
return this.methodParam;
}
/**
* Determine if the HTTP method is supported by browsers (i.e. GET or POST).
*/
protected boolean isMethodBrowserSupported(String method) {
return ("get".equalsIgnoreCase(method) || "post".equalsIgnoreCase(method));
}
/**
* Writes the opening part of the block '<code>form</code>' tag and exposes
* the form object name in the {@link javax.servlet.jsp.PageContext}.
* @param tagWriter the {@link TagWriter} to which the form content is to be written
* @return {@link javax.servlet.jsp.tagext.Tag#EVAL_BODY_INCLUDE}
*/
@Override
protected int writeTagContent(TagWriter tagWriter) throws JspException {
this.tagWriter = tagWriter;
tagWriter.startTag(FORM_TAG);
writeDefaultAttributes(tagWriter);
tagWriter.writeAttribute(ACTION_ATTRIBUTE, resolveAction());
writeOptionalAttribute(tagWriter, METHOD_ATTRIBUTE, isMethodBrowserSupported(getMethod()) ? getMethod() : DEFAULT_METHOD);
writeOptionalAttribute(tagWriter, TARGET_ATTRIBUTE, getTarget());
writeOptionalAttribute(tagWriter, ENCTYPE_ATTRIBUTE, getEnctype());
writeOptionalAttribute(tagWriter, ACCEPT_CHARSET_ATTRIBUTE, getAcceptCharset());
writeOptionalAttribute(tagWriter, ONSUBMIT_ATTRIBUTE, getOnsubmit());
writeOptionalAttribute(tagWriter, ONRESET_ATTRIBUTE, getOnreset());
writeOptionalAttribute(tagWriter, AUTOCOMPLETE_ATTRIBUTE, getAutocomplete());
tagWriter.forceBlock();
if (!isMethodBrowserSupported(getMethod())) {
String composedValue = dataComposer.compose(getMethodParameter(), getMethod(), false);
tagWriter.startTag(INPUT_TAG);
writeOptionalAttribute(tagWriter, TYPE_ATTRIBUTE, "hidden");
writeOptionalAttribute(tagWriter, NAME_ATTRIBUTE, getMethodParameter());
writeOptionalAttribute(tagWriter, VALUE_ATTRIBUTE, composedValue);
tagWriter.endTag();
}
// Expose the form object name for nested tags...
String modelAttribute = resolveModelAttribute();
this.pageContext.setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE);
this.pageContext.setAttribute(COMMAND_NAME_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE);
// Save previous nestedPath value, build and expose current nestedPath value.
// Use request scope to expose nestedPath to included pages too.
this.previousNestedPath =
(String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME,
modelAttribute + PropertyAccessor.NESTED_PROPERTY_SEPARATOR, PageContext.REQUEST_SCOPE);
return EVAL_BODY_INCLUDE;
}
/**
* Autogenerated IDs correspond to the form object name.
*/
@Override
protected String autogenerateId() throws JspException {
return resolveModelAttribute();
}
/**
* {@link #evaluate Resolves} and returns the name of the form object.
* @throws IllegalArgumentException if the form object resolves to <code>null</code>
*/
protected String resolveModelAttribute() throws JspException {
Object resolvedModelAttribute = evaluate(MODEL_ATTRIBUTE, getModelAttribute());
if (resolvedModelAttribute == null) {
throw new IllegalArgumentException(MODEL_ATTRIBUTE + " must not be null");
}
return (String) resolvedModelAttribute;
}
/**
* Resolve the value of the '<code>action</code>' attribute.
* <p>If the user configured an '<code>action</code>' value then
* the result of evaluating this value is used. Otherwise, the
* {@link org.springframework.web.servlet.support.RequestContext#getRequestUri() originating URI}
* is used.
* @return the value that is to be used for the '<code>action</code>' attribute
*/
protected String resolveAction() throws JspException {
dataComposer = (IDataComposer) this.pageContext.getRequest().getAttribute(TagUtils.DATA_COMPOSER);
String action = getAction();
if (StringUtils.hasText(action)) {
if (!action.startsWith("/")) {
String requestUri = getRequestContext().getRequestUri();
int lastSlash = requestUri.lastIndexOf('/');
if (lastSlash >= 0) {
action = requestUri.substring(0, lastSlash) + "/" + action;
} else {
action = "/" + action;
}
}
String displayString = getDisplayString(evaluate(ACTION_ATTRIBUTE, action));
String beginAction = displayString;
if (displayString.contains("?")) {
beginAction = displayString.substring(0, displayString.indexOf("?"));
}
dataComposer.beginRequest(HDIVUtil.getActionMappingName(beginAction));
if (displayString.contains("?")) {
String encodedParams = this.composeQueryString(displayString.substring(displayString.indexOf("?") + 1));
displayString = beginAction + "?" + encodedParams;
}
return displayString;
} else { // action == null
String requestUri = getRequestContext().getRequestUri();
ServletResponse response = this.pageContext.getResponse();
if (response instanceof HttpServletResponse) {
requestUri = ((HttpServletResponse) response).encodeURL(requestUri);
dataComposer.beginRequest(HDIVUtil.getActionMappingName(requestUri));
String queryString = getRequestContext().getQueryString();
if (StringUtils.hasText(queryString)) {
queryString = this.composeQueryString(queryString);
requestUri += "?" + HtmlUtils.htmlEscape(queryString);
}
}
if (StringUtils.hasText(requestUri)) {
return requestUri;
} else {
throw new IllegalArgumentException("Attribute 'action' is required. Attempted to resolve "
+ "against current request URI but request URI was null");
}
}
}
/**
* Removes HDIV parameter from <code>queryString</code> and it composes
* other parameters.
*
* @param queryString query string
* @return queryString without HDIV's parameter
*/
protected String composeQueryString(String queryString) {
String token = null;
StringBuffer result = new StringBuffer();
StringTokenizer st = new StringTokenizer(queryString, "&");
while (st.hasMoreTokens()) {
token = st.nextToken();
String param = token.substring(0, token.indexOf("="));
if (!ignoreParameter(param)) {
String originalValue = this.pageContext.getRequest().getParameter(param);
String val = dataComposer.compose(param, originalValue, false);
if (result.length() > 0) {
result.append("&");
}
result.append(param + "=" + val);
}
}
return result.toString();
}
/**
* @returns Returns true if parameter <code>param</code> must be ignored.
* False otherwise.
*/
protected boolean ignoreParameter(String param) {
String hdivParameter = (String) HDIVUtil.getHttpSession().getAttribute("HDIVParameter");
return param.equalsIgnoreCase(hdivParameter);
}
/**
* Closes the '<code>form</code>' block tag and removes the form object name
* from the {@link javax.servlet.jsp.PageContext}.
*/
@Override
public int doEndTag() throws JspException {
addExtraParameters();
this.tagWriter.endTag();
return EVAL_PAGE;
}
/**
* Adds new parameters to the form that are not been defined with HTML tags.
*/
public void addExtraParameters() throws JspException {
if (!isMethodBrowserSupported(getMethod())) {
// TODO: should not be editable. It is well for now because I know how to solve it...!!
String hdivValue = dataComposer.compose(getMethodParameter(), getMethod(), true);
}
this.addHDIVParameter();
}
/**
* Adds HDIV state as parameter.
*/
protected void addHDIVParameter() throws JspException {
String requestId = dataComposer.endRequest();
if (requestId.length() > 0) {
String hdivState = (String) HDIVUtil.getHttpSession().getAttribute("HDIVParameter");
this.tagWriter.appendValue(this.generateHiddenTag(hdivState, requestId));
}
}
/**
* Renders an HTML <b><input></b> element of type hidden.
*
* @param name hidden parameter name
* @param requestId request identification
* @return HTML <b><input></b> element of type hidden
*/
protected String generateHiddenTag(String name, String requestId) {
StringBuffer hdivParameter = new StringBuffer();
hdivParameter.append("<input type=\"hidden\"");
renderAttribute(hdivParameter, "name", name);
renderAttribute(hdivParameter, "value", requestId);
hdivParameter.append(">\n");
return hdivParameter.toString();
}
/**
* Prepares an attribute if the <code>value</code> is not null, appending
* it to the the given StringBuffer <code>result</code>.
*
* @param result The StringBuffer that output will be appended to.
*/
private void renderAttribute(StringBuffer result, String name, String value) {
if (value != null) {
result.append(" ");
result.append(name);
result.append("=\"");
result.append(value);
result.append("\"");
}
}
public IDataComposer getDataComposer() {
return dataComposer;
}
/**
* Clears the stored {@link TagWriter}.
*/
@Override
public void doFinally() {
super.doFinally();
this.pageContext.removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
this.pageContext.removeAttribute(COMMAND_NAME_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
if (this.previousNestedPath != null) {
// Expose previous nestedPath value.
this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE);
}
else {
// Remove exposed nestedPath value.
this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
}
this.tagWriter = null;
this.previousNestedPath = null;
}
/**
* Override resolve CSS class since error class is not supported.
*/
@Override
protected String resolveCssClass() throws JspException {
return ObjectUtils.getDisplayString(evaluate("cssClass", getCssClass()));
}
/**
* Unsupported for forms.
* @throws UnsupportedOperationException always
*/
@Override
public void setPath(String path) {
throw new UnsupportedOperationException("The 'path' attribute is not supported for forms");
}
/**
* Unsupported for forms.
* @throws UnsupportedOperationException always
*/
@Override
public void setCssErrorClass(String cssErrorClass) {
throw new UnsupportedOperationException("The 'cssErrorClass' attribute is not supported for forms");
}
/**
* @return the tag writer
*/
public TagWriter getTagWriter() {
return tagWriter;
}
}