/* Copyright 2005-2006 Tim Fennell * * 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 net.sourceforge.stripes.tag; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.ParameterName; import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.format.Formatter; import net.sourceforge.stripes.format.FormatterFactory; import net.sourceforge.stripes.localization.LocalizationUtility; import net.sourceforge.stripes.util.CryptoUtil; import net.sourceforge.stripes.validation.BooleanTypeConverter; import net.sourceforge.stripes.validation.ValidationError; import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.validation.ValidationMetadata; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.TryCatchFinally; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Random; import java.util.Stack; /** * Parent class for all input tags in stripes. Provides support methods for retrieving all the * attributes that are shared across form input tags. Also provides accessors for finding the * specified "override" value and for finding the enclosing support tag. * * @author Tim Fennell */ public abstract class InputTagSupport extends HtmlTagSupport implements TryCatchFinally { private String formatType; private String formatPattern; private boolean focus; private boolean syntheticId; /** A list of the errors related to this input tag instance */ protected List<ValidationError> fieldErrors; private boolean fieldErrorsLoaded = false; // used to track if fieldErrors is loaded yet /** The error renderer to be utilized for error output of this input tag */ protected TagErrorRenderer errorRenderer; /** Sets the type of output to format, e.g. date or time. */ public void setFormatType(String formatType) { this.formatType = formatType; } /** Returns the value set with setFormatAs() */ public String getFormatType() { return this.formatType; } /** Sets the named format pattern, or a custom format pattern. */ public void setFormatPattern(String formatPattern) { this.formatPattern = formatPattern; } /** Returns the value set with setFormatPattern() */ public String getFormatPattern() { return this.formatPattern; } /** * Gets the value for this tag based on the current population strategy. The value returned * could be a scalar value, or it could be an array or collection depending on what the * population strategy finds. For example, if the user submitted multiple values for a * checkbox, the default population strategy would return a String[] containing all submitted * values. * * @return Object either a value/values for this tag or null * @throws StripesJspException if the enclosing form tag (which is required at all times, and * necessary to perform repopulation) cannot be located */ protected Object getOverrideValueOrValues() throws StripesJspException { return StripesFilter.getConfiguration().getPopulationStrategy().getValue(this); } /** * Returns a single value for the the value of this field. This can be used to ensure that * only a single value is returned by the population strategy, which is useful in the case * of text inputs etc. which can have only a single value. * * @return Object either a single value or null * @throws StripesJspException if the enclosing form tag (which is required at all times, and * necessary to perform repopulation) cannot be located */ protected Object getSingleOverrideValue() throws StripesJspException { Object unknown = getOverrideValueOrValues(); Object returnValue = null; if (unknown != null && unknown.getClass().isArray()) { if (Array.getLength(unknown) > 0) { returnValue = Array.get(unknown, 0); } } else if (unknown != null && unknown instanceof Collection<?>) { Collection<?> collection = (Collection<?>) unknown; if (collection.size() > 0) { returnValue = collection.iterator().next(); } } else { returnValue = unknown; } return returnValue; } /** * Used during repopulation to query the tag for a value of values provided to the tag * on the JSP. This allows the PopulationStrategy to encapsulate all decisions about * which source to use when repopulating tags. * * @return May return any of String[], Collection or Object */ public Object getValueOnPage() { Object value = getBodyContentAsString(); if (value == null) { try { Method getValue = getClass().getMethod("getValue"); value = getValue.invoke(this); } catch (Exception e) { // Not a lot we can do about this. It's either because the subclass in question // doesn't have a getValue() method (which is ok), or it threw an exception. } } return value; } /** * <p>Locates the enclosing stripes form tag. If no form tag can be found, because the tag * was not enclosed in one on the JSP, an exception is thrown.</p> * * @return FormTag the enclosing form tag on the JSP * @throws StripesJspException if an enclosing form tag cannot be found */ public FormTag getParentFormTag() throws StripesJspException { FormTag parent = getParentTag(FormTag.class); // find the first non-partial parent form tag if (parent != null && parent.isPartial()) { Stack<StripesTagSupport> stack = getTagStack(); ListIterator<StripesTagSupport> iter = stack.listIterator(stack.size()); while (iter.hasPrevious()) { StripesTagSupport tag = iter.previous(); if (tag instanceof FormTag && !((FormTag) tag).isPartial()) { parent = (FormTag) tag; break; } } } if (parent == null) { throw new StripesJspException ("InputTag of type [" + getClass().getName() + "] must be enclosed inside a " + "stripes form tag. If, for some reason, you do not wish to render a complete " + "form you may surround stripes input tags with <s:form partial=\"true\" ...> " + "which will provide support to the input tags but not render the <form> tag."); } return parent; } /** * Utility method for determining if a String value is contained within an Object, where the * object may be either a String, String[], Object, Object[] or Collection. Used primarily * by the InputCheckBoxTag and InputSelectTag to determine if specific check boxes or * options should be selected based on the values contained in the JSP, HttpServletRequest and * the ActionBean. * * @param value the value that we are searching for * @param selected a String, String[], Object, Object[] or Collection (of scalars) denoting the * selected items * @return boolean true if the String can be found, false otherwise */ protected boolean isItemSelected(Object value, Object selected) { // Since this is a checkbox, there could be more than one checked value, which means // this could be a single value type, array or collection if (selected != null) { String stringValue = (value == null) ? "" : format(value, false); if (selected.getClass().isArray()) { int length = Array.getLength(selected); for (int i=0; i<length; ++i) { Object item = Array.get(selected, i); if ( (format(item, false).equals(stringValue)) ) { return true; } } } else if (selected instanceof Collection<?>) { Collection<?> selectedIf = (Collection<?>) selected; for (Object item : selectedIf) { if ( (format(item, false).equals(stringValue)) ) { return true; } } } else { if( format(selected, false).equals(stringValue) ) { return true; } } } // If we got this far without returning, then this is not a selected item return false; } /** * Fetches the localized name for this field if one exists in the resource bundle. Relies on * there being a "name" attribute on the tag, and the pageContext being set on the tag. First * checks for a value of {actionBean FQN}.{fieldName} in the specified bundle, then * {actionPath}.{fieldName} then just "fieldName". * * @return a localized field name if one can be found, or null if one cannot be found. */ public String getLocalizedFieldName() throws StripesJspException { String name = getAttributes().get("name"); return getLocalizedFieldName(name); } /** * Attempts to fetch a "field name" resource from the localization bundle. Delegates * to {@link LocalizationUtility#getLocalizedFieldName(String, String, Class, java.util.Locale)} * * @param name the field name or resource to look up * @return the localized String corresponding to the name provided * @throws StripesJspException */ protected String getLocalizedFieldName(final String name) throws StripesJspException { Locale locale = getPageContext().getRequest().getLocale(); FormTag form = null; try { form = getParentFormTag(); } catch (StripesJspException sje) { /* Do nothing. */} String actionPath = null; Class<? extends ActionBean> beanClass = null; if (form != null) { actionPath = form.getAction(); beanClass = form.getActionBeanClass(); } else { ActionBean mainBean = (ActionBean) getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); if (mainBean != null) { beanClass = mainBean.getClass(); } } return LocalizationUtility.getLocalizedFieldName(name, actionPath, beanClass, locale); } protected ValidationMetadata getValidationMetadata() throws StripesJspException { // find the action bean class we're dealing with Class<? extends ActionBean> beanClass = getParentFormTag().getActionBeanClass(); if (beanClass != null) { // ascend the tag stack until a tag name is found String name = getName(); if (name == null) { InputTagSupport tag = getParentTag(InputTagSupport.class); while (name == null && tag != null) { name = tag.getName(); tag = tag.getParentTag(InputTagSupport.class); } } // check validation for encryption flag return StripesFilter.getConfiguration().getValidationMetadataProvider() .getValidationMetadata(beanClass, new ParameterName(name)); } else { return null; } } /** * Calls {@link #format(Object, boolean)} with {@code forOutput} set to true. * * @param input The object to be formatted * @see #format(Object, boolean) */ protected String format(Object input) { return format(input, true); } /** * Attempts to format an object using the Stripes formatting system. If no formatter can * be found, then a simple String.valueOf(input) will be returned. If the value passed in * is null, then the empty string will be returned. * * @param input The object to be formatted * @param forOutput If true, then the object will be formatted for output to the JSP. Currently, * that means that if encryption is enabled for the ActionBean property with the same * name as this tag then the formatted value will be encrypted before it is returned. */ @SuppressWarnings("unchecked") protected String format(Object input, boolean forOutput) { if (input == null) { return ""; } // format the value FormatterFactory factory = StripesFilter.getConfiguration().getFormatterFactory(); Formatter formatter = factory.getFormatter(input.getClass(), getPageContext().getRequest().getLocale(), this.formatType, this.formatPattern); String formatted = (formatter == null) ? String.valueOf(input) : formatter.format(input); // encrypt the formatted value if required if (forOutput && formatted != null) { try { ValidationMetadata validate = getValidationMetadata(); if (validate != null && validate.encrypted()) formatted = CryptoUtil.encrypt(formatted); } catch (JspException e) { throw new StripesRuntimeException(e); } } return formatted; } /** * Find errors that are related to the form field this input tag represents and place * them in an instance variable to use during error rendering. */ protected void loadErrors() throws StripesJspException { ActionBean actionBean = getActionBean(); if (actionBean != null) { ValidationErrors validationErrors = actionBean.getContext().getValidationErrors(); if (validationErrors != null) { this.fieldErrors = validationErrors.get(getName()); } } } /** * Access for the field errors that occurred on the form input this tag represents * @return List<ValidationError> the list of validation errors for this field */ public List<ValidationError> getFieldErrors() throws StripesJspException { if (!fieldErrorsLoaded) { loadErrors(); fieldErrorsLoaded = true; } return fieldErrors; } /** * Returns true if one or more validation errors exist for the field represented by * this input tag. */ public boolean hasErrors() throws StripesJspException { List<ValidationError> errors = getFieldErrors(); return errors != null && errors.size() > 0; } /** * Fetches the ActionBean associated with the form if one is present. An ActionBean will not * be created (and hence not present) by default. An ActionBean will only be present if the * current request got bound to the same ActionBean as the current form uses. E.g. if we are * re-showing the page as the result of an error, or the same ActionBean is used for a * "pre-Action" and the "post-action". * * @return ActionBean the ActionBean bound to the form if there is one */ public ActionBean getActionBean() throws StripesJspException { return getParentFormTag().getActionBean(); } /** * Final implementation of the doStartTag() method that allows the base InputTagSupport class * to insert functionality before and after the tag performs its doStartTag equivalent * method. Finds errors related to this field and intercepts with a {@link TagErrorRenderer} * if appropriate. * * @return int the value returned by the child class from doStartInputTag() */ @Override public final int doStartTag() throws JspException { getTagStack().push(this); registerWithParentForm(); // Deal with any error rendering if (getFieldErrors() != null) { this.errorRenderer = StripesFilter.getConfiguration() .getTagErrorRendererFactory().getTagErrorRenderer(this); this.errorRenderer.doBeforeStartTag(); } return doStartInputTag(); } /** * Registers the field with the parent form within which it must be enclosed. * @throws StripesJspException if the parent form tag is not found */ protected void registerWithParentForm() throws StripesJspException { getParentFormTag().registerField(this); } /** Abstract method implemented in child classes instead of doStartTag(). */ public abstract int doStartInputTag() throws JspException; /** * Final implementation of the doEndTag() method that allows the base InputTagSupport class * to insert functionality before and after the tag performs its doEndTag equivalent * method. * * @return int the value returned by the child class from doStartInputTag() */ @Override public final int doEndTag() throws JspException { // Wrap in a try/finally because a custom error renderer could throw an // exception, and some containers in their infinite wisdom continue to // cache/pool the tag even after a JSPException is thrown! try { int result = doEndInputTag(); if (getFieldErrors() != null) { this.errorRenderer.doAfterEndTag(); } if (this.focus) { makeFocused(); } return result; } finally { this.errorRenderer = null; this.fieldErrors = null; this.fieldErrorsLoaded = false; this.focus = false; } } /** Rethrows the passed in throwable in all cases. */ public void doCatch(Throwable throwable) throws Throwable { throw throwable; } /** * Used to ensure that the input tag is always removed from the tag stack so that there is * never any confusion about tag-parent hierarchies. */ public void doFinally() { try { getTagStack().pop(); } catch (Throwable t) { /* Suppress anything, because otherwise this might mask any causal exception. */ } } /** * Informs the tag that it should render JavaScript to ensure that it is focused * when the page is loaded. If the tag does not have an 'id' attribute a random * one will be created and set so that the tag can be located easily. * * @param focus true if focus is desired, false otherwise */ public void setFocus(boolean focus) { this.focus = focus; if ( getId() == null ) { this.syntheticId = true; setId("stripes-" + new Random().nextInt()); } } /** Writes out a JavaScript string to set focus on the field as it is rendered. */ protected void makeFocused() throws JspException { try { JspWriter out = getPageContext().getOut(); out.write("<script type=\"text/javascript\">setTimeout(function(){try{var z=document.getElementById('"); out.write(getId()); out.write("');z.focus();"); if ("text".equals(getAttributes().get("type")) || "password".equals(getAttributes().get("type"))) { out.write("z.select();"); } out.write("}catch(e){}},1);</script>"); // Clean up tag state involved with focus this.focus = false; if (this.syntheticId) getAttributes().remove("id"); this.syntheticId = false; } catch (IOException ioe) { throw new StripesJspException("Could not write javascript focus code to jsp writer.", ioe); } } /** Abstract method implemented in child classes instead of doEndTag(). */ public abstract int doEndInputTag() throws JspException; // Getters and setters only below this point. /** * Checks to see if the value provided is either 'disabled' or a value that the * {@link BooleanTypeConverter} believes it true. If so, adds a disabled attribute * to the tag, otherwise does not. */ public void setDisabled(String disabled) { boolean isDisabled = "disabled".equalsIgnoreCase(disabled); if (!isDisabled) { BooleanTypeConverter converter = new BooleanTypeConverter(); isDisabled = converter.convert(disabled, Boolean.class, null); } if (isDisabled) { set("disabled", "disabled"); } else { getAttributes().remove("disabled"); } } public String getDisabled() { return get("disabled"); } /** * <p>Sets the value of the readonly attribute to "readonly" but only when the value passed * in is either "readonly" itself, or is converted to true by the * {@link net.sourceforge.stripes.validation.BooleanTypeConverter}.</p> * * <p>Although not all input tags support the readonly attribute, the method is located here * because it is not a simple one-liner and is used by more than one tag.</p> */ public void setReadonly(String readonly) { boolean isReadOnly = "readonly".equalsIgnoreCase(readonly); if (!isReadOnly) { BooleanTypeConverter converter = new BooleanTypeConverter(); isReadOnly = converter.convert(readonly, Boolean.class, null); } if (isReadOnly) { set("readonly", "readonly"); } else { getAttributes().remove("readonly"); } } /** Gets the HTML attribute of the same name. */ public String getReadonly() { return get("readonly"); } public void setName(String name) { set("name", name); } public String getName() { return get("name"); } public void setSize(String size) { set("size", size); } public String getSize() { return get("size"); } }