package er.directtoweb.delegates; import java.math.BigDecimal; import java.util.Enumeration; import com.webobjects.appserver.WODisplayGroup; import com.webobjects.directtoweb.D2WContext; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSValidation; import er.directtoweb.pages.ERD2WQueryPage; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.validation.ERXValidationFactory; /** * A delegate class for validating user inputs before a query is executed. Validation rules are derived from the D2W * context. * <p> * To disallow a query with no user inputs, create a rule like: * <p> * <code>entity.name = 'Foo' => allowsEmptyQueryValue = "false" (BooleanAssignment)</code> * <p> * To define a validation for a propertyKey, create a rule like: * <p> * <code>entity.name = 'Foo' and propertyKey = 'bar' => allowsEmptyQueryValue = "false" (BooleanAssignment)</code> * <p> * To define a minimum length validation for a (String) propertyKey, create a rule like: * <p> * <code>entity.name = 'Foo' and propertyKey = 'bar' => minimumInputLength = "3" (Assignment)</code> * <p> * Subclasses wishing to implement custom validation logic should implement the {@link #validateQueryValues} method. * The implementation should catch validation exceptions and invoke * {@link er.directtoweb.pages.ERD2WPage#validationFailedWithException(Throwable, Object, String)} with any caught exceptions. To customize * behavior, while retaining the default checks, extend {@link ERDQueryValidationDelegate.DefaultQueryValidationDelegate} * to perform custom validations and then call {@link #validateQueryValues} on the superclass. * * @author Travis Cripps * @d2wKey displayPropertyKeys * @d2wKey maximumInputLength * @d2wKey minimumInputLength * @d2wKey maximumInputValue * @d2wKey minimumInputValue */ public abstract class ERDQueryValidationDelegate { // The validation keys correspond to rule names for validation definitions in the D2W model. public static interface ValidationKeys { public static final String AllowsEmptyQuery = "allowsEmptyQuery"; // Does the page allow a query with no query values? public static final String AllowsEmptyQueryValue = "allowsEmptyQueryValue"; // Does the corresponding property key allow an empty query value? public static final String MaximumInputLength = "maximumInputLength"; // A minimum input length for String attributes. public static final String MinimumInputLength = "minimumInputLength"; // A maximum input length for String attributes. public static final String MaximumInputValue = "maximumInputValue"; // A minimum input value for numeric attributes. public static final String MinimumInputValue = "minimumInputValue"; // A maximum input value for numeric attributes. } // The error keys should correspond to entries in the ValidationTemplate.strings file. public static interface ErrorKeys { public static final String QueryEmpty = "QueryEmpty"; public static final String QueryValueRequired = "QueryValueRequired"; public static final String QueryValueTooShort = "QueryValueTooShort"; public static final String QueryValueTooLong = "QueryValueTooLong"; public static final String QueryValueTooSmall = "QueryValueTooSmall"; public static final String QueryValueTooLarge = "QueryValueTooLarge"; } protected D2WContext d2wContext; /** * Validates the query inputs before executing the query. * @param sender query page whose inputs to validate */ public void validateQuery(ERD2WQueryPage sender) { d2wContext = sender.d2wContext(); // First check to see if there are any query values. WODisplayGroup displayGroup = sender.displayGroup(); if (displayGroup.queryMatch().allKeys().count() == 0 && displayGroup.queryMin().allKeys().count() == 0 && displayGroup.queryMax().allKeys().count() == 0 && displayGroup.queryBindings().allKeys().count() == 0 && !ERXValueUtilities.booleanValueWithDefault(d2wContext.valueForKey(ValidationKeys.AllowsEmptyQuery), true)) { throw ERXValidationFactory.defaultFactory().createCustomException(null, ErrorKeys.QueryEmpty); } // Check the query values. String cachedPropertyKey = d2wContext.propertyKey(); validateQueryValues(sender); d2wContext.setPropertyKey(cachedPropertyKey); } /** * Validates the query input values from the query page's display group. * @param sender query page whose inputs to validate */ public abstract void validateQueryValues(ERD2WQueryPage sender); /** * Gets the D2WContext against which the validation definitions will be evaluated. * from * @return the D2WContext */ public D2WContext d2wContext() { return d2wContext; } /** * Gets the D2W property key corresponding to the display group key by matching the key with one in the D2W * context's <code>displayPropertyKeys</code>. * @param key from the display group * @return the corresponding D2W key, or the original key if not found */ protected String propertyKeyFromDisplayGroupKey(String key) { String result = key; if (result.indexOf(".") > 0) { NSArray propertyKeys = ERXValueUtilities.arrayValueWithDefault(d2wContext().valueForKey("displayPropertyKeys"), NSArray.EmptyArray); boolean found; do { found = propertyKeys.indexOfObject(result) > 0; if (!found) { if (result.indexOf(".") > 0) { result = ERXStringUtilities.keyPathWithoutLastProperty(result); } else { break; } } } while (!found); if (!found) { // Give up. result = key; } } return result; } /** * Determines if the D2W context contains a validation definition for the provided validation key}. * @param key to check * @return true if a validation definition for the given key exists */ public boolean hasValidationDefinitionForKey(String key) { return d2wContext != null && d2wContext.valueForKey(key) != null; } /** * Validates a string value, checking minimumInputLength and maximumInputLength. * @param value to validate * @param key of the property to validate * @throws NSValidation.ValidationException when the validation fails */ public void validateStringValueForKey(String value, String key) throws NSValidation.ValidationException { int minimumLength = ERXValueUtilities.intValueWithDefault(d2wContext().valueForKey(ValidationKeys.MinimumInputLength), 0); if (null == value) { if (minimumLength > 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, ErrorKeys.QueryValueRequired); } } else { int maximumLength = ERXValueUtilities.intValueWithDefault(d2wContext().valueForKey(ValidationKeys.MaximumInputLength), -1); if (minimumLength > 0 && value.length() < minimumLength) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, ErrorKeys.QueryValueTooShort); } if (maximumLength > 0 && value.length() > maximumLength) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, ErrorKeys.QueryValueTooLong); } } } /** * Validates a string value, checking minimumInputValue and maximumInputValue. * @param value to validate * @param key of the property to validate * @throws NSValidation.ValidationException when the validation fails */ public void validateNumericValueForKey(Number value, String key) throws NSValidation.ValidationException { if (null == value) { if (d2wContext.valueForKey(ValidationKeys.MinimumInputValue) != null) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueRequired"); } } else { EOAttribute attribute = d2wContext.attribute(); String valueType = attribute.valueType(); if ("s".equals(valueType) || "i".equals(valueType) || "l".equals(valueType)) { // Short or Integer or Long long longValue = value.longValue(); if (hasValidationDefinitionForKey(ValidationKeys.MinimumInputValue)) { Long minimumValue = ERXValueUtilities.longValue(d2wContext().valueForKey(ValidationKeys.MinimumInputValue)); if (minimumValue.compareTo(longValue) > 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, ErrorKeys.QueryValueTooSmall); } } if (hasValidationDefinitionForKey(ValidationKeys.MaximumInputValue)) { Long maximumValue = ERXValueUtilities.longValue(d2wContext().valueForKey(ValidationKeys.MaximumInputValue)); if (maximumValue.compareTo(longValue) < 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueTooLarge"); } } } else if ("f".equals(valueType) || "d".equals(valueType)) { // Float or Double double doubleValue = value.doubleValue(); if (hasValidationDefinitionForKey(ValidationKeys.MinimumInputValue)) { Double minimumValue = ERXValueUtilities.doubleValue(d2wContext().valueForKey(ValidationKeys.MinimumInputValue)); if (minimumValue.compareTo(doubleValue) > 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueTooSmall"); } } if (hasValidationDefinitionForKey(ValidationKeys.MaximumInputValue)) { Double maximumValue = ERXValueUtilities.doubleValue(d2wContext().valueForKey(ValidationKeys.MaximumInputValue)); if (maximumValue.compareTo(doubleValue) < 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueTooLarge"); } } } else if ("B".equals(valueType)) { // BigDecimal BigDecimal bdValue = (value instanceof BigDecimal) ? (BigDecimal)value : new BigDecimal(value.doubleValue()); if (hasValidationDefinitionForKey(ValidationKeys.MinimumInputValue)) { BigDecimal minimumValue = ERXValueUtilities.bigDecimalValue(d2wContext().valueForKey(ValidationKeys.MinimumInputValue)); if (minimumValue.compareTo(bdValue) > 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueTooSmall"); } } if (hasValidationDefinitionForKey(ValidationKeys.MaximumInputValue)) { BigDecimal maximumValue = ERXValueUtilities.bigDecimalValue(d2wContext().valueForKey(ValidationKeys.MaximumInputValue)); if (maximumValue.compareTo(bdValue) < 0) { throw ERXValidationFactory.defaultFactory().createCustomException(null, key, value, "QueryValueTooLarge"); } } } } } /** * A "default" implementation of a query validation delegate, which simply validates each key in the query page's * display group against validation definitions from the D2W rules. */ public static class DefaultQueryValidationDelegate extends ERDQueryValidationDelegate { private ERD2WQueryPage queryPage; @Override public void validateQueryValues(ERD2WQueryPage sender) { queryPage = sender; WODisplayGroup displayGroup = queryPage.displayGroup(); _validateQueryValues(displayGroup.queryMatch()); _validateQueryValues(displayGroup.queryMin()); _validateQueryValues(displayGroup.queryMax()); _validateQueryValues(displayGroup.queryBindings()); } /** * Validates the values in the query dictionary. * @param queryDict to validate. */ private void _validateQueryValues(NSDictionary queryDict) { for (Enumeration keysEnum = queryDict.keyEnumerator(); keysEnum.hasMoreElements();) { String key = (String)keysEnum.nextElement(); Object value = queryDict.objectForKey(key); try { validateValueForQueryKey(value, key); } catch (NSValidation.ValidationException ve) { queryPage.validationFailedWithException(ve, null, ve.key()); } } } /** * Validates the value of a particular key in the query dictionary. * @param value to validate * @param key of the property to validate * @throws NSValidation.ValidationException when the validation fails */ public void validateValueForQueryKey(Object value, String key) throws NSValidation.ValidationException { if( value instanceof NSKeyValueCoding.Null) { value = null; } D2WContext d2wContext = d2wContext(); String propertyKey = propertyKeyFromDisplayGroupKey(key); d2wContext().setPropertyKey(propertyKey); if (null == value && !ERXValueUtilities.booleanValueWithDefault(d2wContext.valueForKey(ValidationKeys.AllowsEmptyQueryValue), true)) { throw ERXValidationFactory.defaultFactory().createCustomException(null, propertyKey, value, "QueryValueRequired"); } EOAttribute attribute = null; if (ERXValueUtilities.booleanValue(d2wContext.valueForKey("isAttribute"))) { attribute = d2wContext.attribute(); } else { EORelationship relationship = d2wContext.relationship(); if (relationship != null && !(value instanceof EOEnterpriseObject)) { String keyWhenRelationship = (String)d2wContext.valueForKey("keyWhenRelationship"); if (keyWhenRelationship != null) { EOEntity destinationEntity = relationship.destinationEntity(); attribute = destinationEntity.attributeNamed(keyWhenRelationship); } } } if (attribute != null) { String valueClassName = attribute.className(); if (String.class.getName().equals(valueClassName) && value instanceof String) { validateStringValueForKey((String)value, propertyKey); } else if (Number.class.getName().equals(valueClassName) || BigDecimal.class.getName().equals(valueClassName)) { validateNumericValueForKey((Number)value, propertyKey); } } } } }