/* 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.StripesConstants;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.validation.ValidationError;
import net.sourceforge.stripes.validation.ValidationErrors;
import net.sourceforge.stripes.exception.StripesJspException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.BodyTag;
import java.io.IOException;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* <p>The errors tag has two modes, one where it displays all validation errors in a list
* and a second mode when there is a single enclosed field-error tag that has no name attribute
* in which case this tag iterates over the body, displaying each error in turn in place
* of the field-error tag.</p>
*
* <p>In the first mode, where the default output is used, it is possible to change the output
* for the entire application using a set of resources in the error messages bundle
* (StripesResources.properties unless you have configured another). If the properties are
* undefined, the tag will output the text "Validation Errors" in a div with css class errorHeader,
* then output an unordered list of error messages. The following four resource strings
* (shown with their default values) can be modified to create different default output:</p>
*
* <ul>
* <li>stripes.errors.header={@literal <div class="errorHeader">Validation Errors</div><ul>}</li>
* <li>stripes.errors.footer={@literal </ul>}</li>
* <li>stripes.errors.beforeError={@literal <li>}</li>
* <li>stripes.errors.afterError={@literal </li>}</li>
* </ul>
*
* <p>The errors tag can also be used to display errors for a single field by supplying it
* with a 'field' attribute which matches the name of a field on the page. In this case the tag
* will display only if errors exist for the named field. In this mode the tag will first look for
* resources named:</p>
*
* <ul>
* <li>stripes.fieldErrors.header</li>
* <li>stripes.fieldErrors.footer</li>
* <li>stripes.fieldErrors.beforeError</li>
* <li>stripes.fieldErrors.afterError</li>
* </ul>
*
* <p>If the {@code fieldErrors} resources cannot be found, the tag will default to using the
* same resources and defaults as when displaying for all fields.</p>
*
* <p>Similar to the above, field specific, manner of display the errors tag can also be used
* to output only errors not associated with a field, i.e. global errors. This is done by setting
* the {@code globalErrorsOnly} attribute to true.</p>
*
* <p>This tag has several ways of being attached to the errors of a specific action request.
* If the tag is inside a form tag, it will display only errors that are associated
* with that form. If supplied with an 'action' attribute, it will display errors only errors
* associated with a request to that URL. Finally, if neither is the case, it
* will always display as described in the paragraph above.</p>
*
* @author Greg Hinkle, Tim Fennell
*/
public class ErrorsTag extends HtmlTagSupport implements BodyTag {
private static final Log log = Log.getInstance(ErrorsTag.class);
/** The header that will be emitted if no header is defined in the resource bundle. */
public static final String DEFAULT_HEADER =
"<div class=\"errorHeader\">Validation Errors</div><ul>";
/** The footer that will be emitted if no footer is defined in the resource bundle. */
public static final String DEFAULT_FOOTER = "</ul>";
/**
* True if this tag will display errors, otherwise false. This is determined by the logic
* laid out in the class level Javadoc around whether this errors tag is for the action
* that was submitted in the request.
*/
private boolean display = false;
/**
* True if this tag contains a field-error child tag, which controls
* the place of output of each error
*/
private boolean nestedErrorTagPresent = false;
/** Sets the form action for which errors should be displayed. */
private String action;
/** An optional attribute that declares a particular field to output errors for. */
private String field;
/** An optional attribute that specified to display only the global errors. */
private boolean globalErrorsOnly;
/** The collection of errors that match the filtering conditions */
private SortedSet<ValidationError> allErrors;
/** An iterator of the list of matched errors */
private Iterator<ValidationError> errorIterator;
/** The error displayed in the current iteration */
private ValidationError currentError;
/** An index of the error being displayed - zero based */
private int index = 0;
/**
* Called by the IndividualErrorTag to fetch the current error from the set being iterated.
*
* @return The error displayed for this iteration of the errors tag
*/
public ValidationError getCurrentError() {
this.nestedErrorTagPresent = true;
return currentError;
}
/** Returns true if the error displayed is the first matching error. */
public boolean isFirst() {
return (this.allErrors.first() == this.currentError);
}
/** Returns true if the error displayed is the last matching error. */
public boolean isLast() {
return (this.allErrors.last() == currentError);
}
/** Sets the (optional) action of the form to display errors for, if they exist. */
public void setAction(String action) {
this.action = action;
}
/** Returns the value set with setAction(). */
public String getAction() {
return this.action;
}
/**
* Sets the action attribute by figuring out what ActionBean class is identified
* and then in turn finding out the appropriate URL for the ActionBean.
*
* @param beanclass the FQN of an ActionBean class, or a Class object for one.
*/
public void setBeanclass(Object beanclass) throws StripesJspException {
String url = getActionBeanUrl(beanclass);
if (url == null) {
throw new StripesJspException("The 'beanclass' attribute provided could not be " +
"used to identify a valid and configured ActionBean. The value supplied was: " +
beanclass);
}
else {
this.action = url;
}
}
/** Sets the (optional) name of a field to display errors for, if errors exist. */
public void setField(String field) {
this.field = field;
}
/** Gets the value set with setField(). */
public String getField() {
return field;
}
/** Indicated whether the tag is displaying only global errors. */
public boolean isGlobalErrorsOnly() { return globalErrorsOnly; }
/** Tells the tag to display (or not) only global errors and no field level errors. */
public void setGlobalErrorsOnly(boolean globalErrorsOnly) {
this.globalErrorsOnly = globalErrorsOnly;
}
/**
* Determines if the tag should display errors based on the action that it is displaying for,
* and then fetches the appropriate list of errors and makes sure it is non-empty.
*
* @return SKIP_BODY if the errors are not to be output, or there aren't any<br/>
* EVAL_BODY_TAG if there are errors to display
*/
@Override
public int doStartTag() throws JspException {
HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest();
ActionBean mainBean = (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN);
FormTag formTag = getParentTag(FormTag.class);
ValidationErrors errors = null;
// If we are supplied with an 'action' attribute then display the errors
// only if that action matches the 'action' of the current action bean
if (getAction() != null) {
if (mainBean != null) {
String mainAction = StripesFilter.getConfiguration()
.getActionResolver().getUrlBinding(mainBean.getClass());
if (getAction().equals(mainAction)) {
errors = mainBean.getContext().getValidationErrors();
}
}
}
// Else we don't have an 'action' attribute, so see if we are nested in
// a form tag
else if (formTag != null) {
ActionBean formBean = formTag.getActionBean();
if (formBean != null) {
errors = formBean.getContext().getValidationErrors();
}
}
// Else if no name was set, and we're not in a action tag, we're global and ok to display
else if (mainBean != null) {
errors = mainBean.getContext().getValidationErrors();
}
// If we found some errors that are applicable for display, figure out what to do
if (errors != null) {
// Using a set ensures that duplicate messages get filtered out, which can
// happen during multi-row validation
this.allErrors = new TreeSet<ValidationError>(new ErrorComparator());
if (this.field != null) {
// we're filtering for a specific field
List<ValidationError> fieldErrors = errors.get(this.field);
if (fieldErrors != null) {
this.allErrors.addAll(fieldErrors);
}
}
else if (this.globalErrorsOnly) {
List<ValidationError> globalErrors = errors.get(ValidationErrors.GLOBAL_ERROR);
if (globalErrors != null) {
this.allErrors.addAll(globalErrors);
}
}
else {
for (List<ValidationError> fieldErrors : errors.values()) {
if (fieldErrors != null) {
this.allErrors.addAll(fieldErrors);
}
}
}
}
// Make sure that after all this we really do have some errors
if (this.allErrors != null && this.allErrors.size() > 0) {
this.display = true;
this.errorIterator = this.allErrors.iterator();
this.currentError = this.errorIterator.next(); // load up the first error
return EVAL_BODY_BUFFERED;
}
else {
this.display = false;
return SKIP_BODY;
}
}
/** Sets the context variables for the current error and index */
public void doInitBody() throws JspException {
// Apply TEI attributes
getPageContext().setAttribute("index", this.index);
getPageContext().setAttribute("error", this.currentError);
}
/**
* Manages iteration, running again if there are more errors to display. If there is no
* nested FieldError tag, will ensure that the body is evaluated only once.
*
* @return EVAL_BODY_TAG if there are more errors to display, SKIP_BODY otherwise
*/
public int doAfterBody() throws JspException {
if (this.display && this.nestedErrorTagPresent && this.errorIterator.hasNext()) {
this.currentError = this.errorIterator.next();
this.index++;
// Reapply TEI attributes
getPageContext().setAttribute("index", this.index);
getPageContext().setAttribute("error", this.currentError);
return EVAL_BODY_BUFFERED;
}
else {
return SKIP_BODY;
}
}
/**
* Output the error list if this was an empty body tag and we're fully controlling output*
*
* @return EVAL_PAGE always
* @throws JspException
*/
@Override
public int doEndTag() throws JspException {
try {
JspWriter writer = getPageContext().getOut();
if (this.display && !this.nestedErrorTagPresent) {
// Output all errors in a standard format
Locale locale = getPageContext().getRequest().getLocale();
ResourceBundle bundle = null;
try {
bundle = StripesFilter.getConfiguration()
.getLocalizationBundleFactory().getErrorMessageBundle(locale);
}
catch (MissingResourceException mre) {
log.warn("The errors tag could not find the error messages resource bundle. ",
"As a result default headers/footers etc. will be used. Check that ",
"you have a StripesResources.properties in your classpath (unless ",
"of course you have configured a different bundle).");
}
// Fetch the header and footer
String header = getResource(bundle, "header", DEFAULT_HEADER);
String footer = getResource(bundle, "footer", DEFAULT_FOOTER);
String openElement = getResource(bundle, "beforeError", "<li>");
String closeElement = getResource(bundle, "afterError", "</li>");
// Write out the error messages
writer.write(header);
for (ValidationError fieldError : this.allErrors) {
String message = fieldError.getMessage(locale);
if (message != null && message.length() > 0) {
writer.write(openElement);
writer.write(message);
writer.write(closeElement);
}
}
writer.write(footer);
}
else if (this.display && this.nestedErrorTagPresent) {
// Output the collective body content
getBodyContent().writeOut(writer);
}
// Reset the instance state in case the container decides to pool the tag
this.display = false;
this.nestedErrorTagPresent = false;
this.allErrors = null;
this.errorIterator = null;
this.currentError = null;
this.index = 0;
return EVAL_PAGE;
}
catch (IOException e) {
JspException jspe = new JspException("IOException encountered while writing errors " +
"tag to the JspWriter.", e);
log.warn(jspe);
throw jspe;
}
}
/**
* Utility method that is used to lookup the resources used for the errors header,
* footer, and the strings that go before and after each error.
*
* @param bundle the bundle to look up the resource from
* @param name the name of the resource to lookup (prefixes will be added)
* @param fallback a value to return if no resource can be found
* @return the value to use for the named resource
*/
protected String getResource(ResourceBundle bundle, String name, String fallback) {
if (bundle == null) {
return fallback;
}
String resource = null;
if (this.field != null) {
try { resource = bundle.getString("stripes.fieldErrors." + name); }
catch (MissingResourceException mre) { /* Do nothing */ }
}
if (resource == null) {
try { resource = bundle.getString("stripes.errors." + name); }
catch (MissingResourceException mre) { resource = fallback; }
}
return resource;
}
/**
* Inner class Comparator used to provide a consistent ordering of validation errors.
* Sorting is done by field name (the programmatic one, not the user visible one). Errors
* without field names sort to the top since it is assumed that these are global errors
* as oppose to field specific ones.
*/
private static class ErrorComparator implements Comparator<ValidationError> {
public int compare(ValidationError e1, ValidationError e2) {
// Identical errors should be suppressed
if (e1.equals(e2)) {
return 0;
}
String fn1 = e1.getFieldName();
String fn2 = e2.getFieldName();
boolean e1Global = fn1 == null || fn1.equals(ValidationErrors.GLOBAL_ERROR);
boolean e2Global = fn2 == null || fn2.equals(ValidationErrors.GLOBAL_ERROR);
// Sort globals above non-global errors
if (e1Global && !e2Global) {
return -1;
}
if (e2Global && !e1Global) {
return 1;
}
if (fn1 == null && fn2 == null) {
return 0;
}
// Then sort by field name, and if field names match make the first one come first
int result = e1.getFieldName().compareTo(e2.getFieldName());
if (result == 0) {result = 1;}
return result;
}
}
}