package com.gwt.mvp.client.validation; import java.util.EmptyStackException; import java.util.LinkedList; import java.util.List; import java.util.Stack; /** * */ public class Errors { private static String NESTED_SEPARATOR = "."; private final List<ErrorItem> items = new LinkedList<ErrorItem>(); private String nestedPath = ""; private Stack<String> pathStack = new Stack<String>(); public Errors() { super(); } void reject(String field, String errorCode) { reject(field, errorCode, null, null); } void reject(String field, String errorCode, String defaultMessage) { reject(field, errorCode, null, defaultMessage); } /** * Register a field error for the specified field of the current object, using the given error * description. * <p>The field name may be <code>null</code> or empty String to indicate * the current object itself rather than a field of it. This may result * in a corresponding field error within the nested object graph or a * global error if the current object is the top object. * @param field the field name (may be <code>null</code> or empty String) * @param errorCode error code, interpretable as a message key * @param errorArgs error arguments, for argument binding via MessageFormat * (can be <code>null</code>) * @param defaultMessage fallback default message */ void reject(String field, String errorCode, Object[] errorArgs, String defaultMessage) { items.add(new ErrorItem(fixedField(field), errorCode, errorArgs, defaultMessage)); } /** * Return if there were any errors. */ boolean hasErrors() { return !items.isEmpty(); } /** * Allow context to be changed so that standard validators can validate * subtrees. Reject calls prepend the given path to the field names. * <p>For example, an address validator could validate the subobject * "address" of a customer object. * @param nestedPath nested path within this object, * e.g. "address" (defaults to "", <code>null</code> is also acceptable). * Can end with a dot: both "address" and "address." are valid. */ public void setNestedPath(final String nestedPath) { doSetNestedPath(nestedPath); pathStack.clear(); } /** * Return the current nested path of this {@link Errors} object. * <p>Returns a nested path with a dot, i.e. "address.", for easy * building of concatenated paths. Default is an empty String. */ public String getNestedPath() { return nestedPath; } /** * Push the given sub path onto the nested path stack. * <p>A {@link #popNestedPath()} call will reset the original * nested path before the corresponding * <code>pushNestedPath(String)</code> call. * <p>Using the nested path stack allows to set temporary nested paths * for subobjects without having to worry about a temporary path holder. * <p>For example: current path "spouse.", pushNestedPath("child") -> * result path "spouse.child."; popNestedPath() -> "spouse." again. * @param subPath the sub path to push onto the nested path stack * @see #popNestedPath */ public void pushNestedPath(final String subPath) { pathStack.push(getNestedPath()); doSetNestedPath(getNestedPath() + subPath); } /** * Pop the former nested path from the nested path stack. * @throws IllegalStateException if there is no former nested path on the stack * @see #pushNestedPath */ public void popNestedPath() throws IllegalStateException { try { doSetNestedPath(pathStack.pop()); } catch (EmptyStackException ex) { throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); } } /** * Actually set the nested path. * Delegated to by setNestedPath and pushNestedPath. */ protected void doSetNestedPath(final String nestedPath) { if (nestedPath == null) { this.nestedPath = ""; } else { this.nestedPath = nestedPath; if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_SEPARATOR)) { this.nestedPath += NESTED_SEPARATOR; } } } /** * Transform the given field into its full path, * regarding the nested path of this instance. */ protected String fixedField(final String field) { if (field != null && !"".equals(field)) { return getNestedPath() + field; } else { String path = getNestedPath(); return (path.endsWith(NESTED_SEPARATOR) ? path.substring(0, path.length() - NESTED_SEPARATOR.length()) : path); } } /** * Get all errors associated with the given field. * <p>Implementations should support not only full field names like * "name" but also pattern matches like "na*" or "address.*". * @param field the field name * @return a List of {@link FieldError} instances */ private List<ErrorItem> getFieldErrors(String field) { List<ErrorItem> result = new LinkedList<ErrorItem>(); String fixedField = fixedField(field); for (ErrorItem item : items) { if (isMatchingFieldError(fixedField, item)) { result.add(item); } } return result; } /** * Check whether the given ErrorItem matches the given field. * @param field the field that we are looking up FieldErrors for * @param fieldError the candidate ErrorItem * @return whether the ErrorItem matches the given field */ private boolean isMatchingFieldError(String field, ErrorItem item) { return (field.equals(item.getField()) || (field.endsWith("*") && item.getField().startsWith(field.substring(0, field.length() - 1)))); } /** * Inner class for error management. */ private class ErrorItem { private final String field; private final String errorCode; private final Object[] arguments; private final String defaultMessage; public ErrorItem(String field, String errorCode, Object[] arguments, String defaultMessage) { this.field = field; this.errorCode = errorCode; this.arguments = arguments; this.defaultMessage = defaultMessage; } public String getField() { return field; } } }