package net.sourceforge.stripes.tag; import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.BodyTag; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.ajax.JavaScriptBuilder; import net.sourceforge.stripes.controller.ParameterName; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.localization.LocalizationUtility; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.bean.PropertyExpression; import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation; import net.sourceforge.stripes.validation.ValidationMetadata; import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** * <p>Field metadata tag for use with the Stripes framework. Exposes field properties via JavaScript to * allow client side validation. If this tag has a body it will be wrapped with JavaScript tags for * convenience.</p> * * @author Aaron Porter * */ public class FieldMetadataTag extends HtmlTagSupport implements BodyTag { /** Log used to log error and debugging information for this class. */ private static final Log log = Log.getInstance(FormTag.class); /** Name of variable to hold metadata. */ private String var; /** Optional comma separated list of additional fields to expose. */ private String fields; /** Set to true to include type information for all fields. */ private boolean includeType = false; /** Set to true to include the fully qualified class name for all fields. */ private boolean fqn = false; /** Stores the value of the action attribute before the context gets appended. */ private String actionWithoutContext; public FormTag getForm() { return getParentTag(FormTag.class); } /** * Builds a string that contains field metadata in a JavaScript object. * * @return JavaScript object containing field metadata */ private String getMetadata() { ActionBean bean = null; String action = getAction(); FormTag form = getForm(); if (form != null) { if (action != null) log.warn("Parameters action and/or beanclass specified but field-metadata tag is inside of a Stripes form tag. The bean will be pulled from the form tag."); action = form.getAction(); } if (form != null) bean = form.getActionBean(); Class<? extends ActionBean> beanClass = null; if (bean != null) beanClass = bean.getClass(); else if (action != null) { beanClass = StripesFilter.getConfiguration().getActionResolver().getActionBeanType(action); if (beanClass != null) { try { bean = StripesFilter.getConfiguration().getObjectFactory().newInstance(beanClass); } catch (Exception e) { log.error(e); return null; } } } if (beanClass == null) { log.error("Couldn't determine ActionBean class from FormTag! One of the following conditions must be met:\r\n\t", "1. Include this tag inside of a stripes:form tag\r\n\t", "2. Use the action parameter\r\n\t", "3. Use the beanclass parameter"); return null; } ValidationMetadataProvider metadataProvider = StripesFilter.getConfiguration() .getValidationMetadataProvider(); if (metadataProvider == null) { log.error("Couldn't get ValidationMetadataProvider!"); return null; } Map<String, ValidationMetadata> metadata = metadataProvider .getValidationMetadata(beanClass); StringBuilder sb = new StringBuilder("{\r\n\t\t"); Set<String> fields = new HashSet<String>(); if (form != null) { for (String field : form.getRegisteredFields()) { fields.add(new ParameterName(field).getStrippedName()); } } if ((this.fields != null) && (this.fields.trim().length() > 0)) fields.addAll(Arrays.asList(this.fields.split(","))); else if (form == null) { log.error("Fields attribute is required when field-metadata tag isn't inside of a Stripes form tag."); return null; } boolean first = true; Locale locale = getPageContext().getRequest().getLocale(); for (String field : fields) { PropertyExpressionEvaluation eval; try { eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(field), bean); } catch (Exception e) { continue; } Class<?> fieldType = eval.getType(); ValidationMetadata data = metadata.get(field); StringBuilder fieldInfo = new StringBuilder(); if (fieldType.isPrimitive() || Number.class.isAssignableFrom(fieldType) || Date.class.isAssignableFrom(fieldType) || includeType) { fieldInfo.append("type:").append( JavaScriptBuilder.quote(fqn ? fieldType.getName() : fieldType .getSimpleName())); } Class<?> typeConverterClass = null; if (data != null) { if (fieldInfo.length() > 0) fieldInfo.append(','); fieldInfo.append("required:").append(data.required()) .append(",ignore:").append(data.ignore()) .append(",encrypted:").append(data.encrypted()) .append(",trim:").append(data.trim()); if (data.on() != null) { fieldInfo.append(",on:["); Iterator<String> it = data.on().iterator(); while (it.hasNext()) { fieldInfo.append(JavaScriptBuilder.quote(it.next())); if (it.hasNext()) fieldInfo.append(","); } fieldInfo.append("]"); } if (data.mask() != null) fieldInfo.append(",mask:").append("new RegExp(") .append(JavaScriptBuilder.quote("^" + data.mask().toString() + "$")) .append(")"); if (data.minlength() != null) fieldInfo.append(",minlength:").append(data.minlength()); if (data.maxlength() != null) fieldInfo.append(",maxlength:").append(data.maxlength()); if (data.minvalue() != null) fieldInfo.append(",minvalue:").append(data.minvalue()); if (data.maxvalue() != null) fieldInfo.append(",maxvalue:").append(data.maxvalue()); String label = data.label(); if (data.label() == null) { label = LocalizationUtility.getLocalizedFieldName(field, form == null ? null : form.getAction(), form == null ? null : form.getActionBeanClass(), locale); } if (label != null) fieldInfo.append(",label:").append(JavaScriptBuilder.quote(label)); typeConverterClass = data.converter(); } // If we couldn't get the converter from the validation annotation // try to get it from the TypeConverterFactory if (typeConverterClass == null) { try { typeConverterClass = StripesFilter.getConfiguration().getTypeConverterFactory() .getTypeConverter(fieldType, pageContext.getRequest().getLocale()) .getClass(); } catch (Exception e) { // Just ignore it } } if (typeConverterClass != null) { fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("typeConverter:") .append( JavaScriptBuilder.quote(fqn ? typeConverterClass.getName() : typeConverterClass.getSimpleName())); } if (fieldInfo.length() > 0) { if (first) first = false; else sb.append(",\r\n\t\t"); sb.append(JavaScriptBuilder.quote(field)).append(":{"); sb.append(fieldInfo); sb.append("}"); } } sb.append("\r\n\t}"); return sb.toString(); } public FieldMetadataTag() { getAttributes().put("type", "text/javascript"); } public void doInitBody() throws JspException { } public int doAfterBody() throws JspException { return SKIP_BODY; } @Override public int doStartTag() throws JspException { getPageContext().setAttribute(getVar(), new Var(getMetadata())); return EVAL_BODY_BUFFERED; } @Override public int doEndTag() throws JspException { JspWriter writer = getPageContext().getOut(); String body = getBodyContentAsString(); if (body != null) { try { String contentType = getPageContext().getResponse().getContentType(); // Catches application/x-javascript, text/javascript, and text/ecmascript boolean pageIsScript = contentType != null && contentType.toLowerCase().contains("ascript"); // Don't write the script tags if this page is a script if (!pageIsScript) { writeOpenTag(writer, "script"); writer.write("//<![CDATA[\r\n"); } writer.write(body); if (!pageIsScript) { writer.write("\r\n//]]>"); writeCloseTag(writer, "script"); } } catch (IOException ioe) { throw new StripesJspException("IOException while writing output in LinkTag.", ioe); } } // Only keep the type attribute between uses String type = getAttributes().get("type"); getAttributes().clear(); getAttributes().put("type", type); return SKIP_BODY; } public String getVar() { return var; } /** * Sets the name of the variable to hold metadata. * * @param var the name of the attribute that will contain field metadata */ public void setVar(String var) { this.var = var; } public String getFields() { return fields; } /** * Optional comma separated list of additional fields to expose. Any fields that have * already been added to the Stripes form tag will automatically be included. * * @param fields comma separated list of field names */ public void setFields(String fields) { this.fields = fields; } public boolean isIncludeType() { return includeType; } /** * Set to true to include type information for all fields. By default, type information is only * included for primitives, numbers, and dates. * * @param includeType include type info for all fields */ public void setIncludeType(boolean includeType) { this.includeType = includeType; } public boolean isFqn() { return fqn; } /** * Set to true to include the fully qualified class name for all fields. * * @param fqn include fully qualified class name for all fields */ public void setFqn(boolean fqn) { this.fqn = fqn; } /** * Sets the action for the form. If the form action begins with a slash, and does not already * contain the context path, then the context path of the web application will get prepended to * the action before it is set. In general actions should be specified as "absolute" * paths within the web application, therefore allowing them to function correctly regardless of * the address currently shown in the browser's address bar. * * @param action the action path, relative to the root of the web application */ public void setAction(String action) { // Use the action resolver to figure out what the appropriate URL binding if for // this path and use that if there is one, otherwise just use the action passed in String binding = StripesFilter.getConfiguration().getActionResolver() .getUrlBindingFromPath(action); if (binding != null) { this.actionWithoutContext = binding; } else { this.actionWithoutContext = action; } } public String getAction() { return this.actionWithoutContext; } /** * Sets the 'action' attribute by inspecting the bean class provided and asking the current * ActionResolver what the appropriate URL is. * * @param beanclass the String FQN of the class, or a Class representing the class * @throws StripesJspException if the URL cannot be determined for any reason, most likely * because of a mis-spelled class name, or a class that's not an ActionBean */ public void setBeanclass(Object beanclass) throws StripesJspException { String url = getActionBeanUrl(beanclass); if (url == null) { throw new StripesJspException( "Could not determine action from 'beanclass' supplied. " + "The value supplied was '" + beanclass + "'. Please ensure that this bean type " + "exists and is in the classpath. If you are developing a page and the ActionBean " + "does not yet exist, consider using the 'action' attribute instead for now."); } else { setAction(url); } } /** Corresponding getter for 'beanclass', will always return null. */ public Object getBeanclass() { return null; } /** * This is what is placed into the request attribute. It allows us to * get the field metadata as well as the form id. */ public class Var { private String fieldMetadata, formId; private Var(String fieldMetadata) { this.fieldMetadata = fieldMetadata; FormTag form = getForm(); if (form != null) { if (form.getId() == null) form.setId("stripes-" + new Random().nextInt()); this.formId = form.getId(); } } @Override public String toString() { return fieldMetadata; } public String getFormId() { return formId; } } }