/* * Copyright (C) NetStruxr, Inc. All rights reserved. * * This software is published under the terms of the NetStruxr * Public Software License version 0.5, a copy of which has been * included with this distribution in the LICENSE.NPL file. */ package er.extensions.validation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Hashtable; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSValidation.ValidationException; import er.extensions.eof.ERXConstant; import er.extensions.eof.ERXEntityClassDescription; import er.extensions.foundation.ERXMultiKey; import er.extensions.foundation.ERXSimpleTemplateParser; import er.extensions.foundation.ERXSystem; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; /** * The validation factory controls creating validation * exceptions, both from model thrown exceptions and * custom validation exceptions. The factory is responsible * for resolving validation templates for validation * exceptions and generating validation messages. */ public class ERXValidationFactory { private final static Logger log = LoggerFactory.getLogger(ERXValidationFactory.class); /** holds a reference to the default validation factory */ private static ERXValidationFactory _defaultFactory; /** holds a reference to the default validation delegate */ // FIXME: This should be a weak reference private static Object _defaultValidationDelegate = null; /** holds the default mappings that map model thrown validation strings to exception types */ private static NSDictionary<String, String> _mappings; /** holds the value 'ValidationTemplate.' */ public static final String VALIDATION_TEMPLATE_PREFIX = "ValidationTemplate."; /** holds the method name 'messageForException' */ // FIXME: This is better done with an NSSelector and using the method: implementedByObject private static final String EDI_MFE_METHOD_NAME = "messageForException"; /** holds the method name 'templateForException' */ private static final String EDI_TFE_METHOD_NAME = "templateForException"; /** holds the class argument array for delegate validation exception messages */ private static final Class[] EDI_FE_ARGS = new Class[] { ERXValidationException.class }; /** Regular ERXValidationException constructor parameters */ private static Class[] _regularConstructor = new Class[] { String.class, Object.class, String.class, Object.class }; /** holds the marker for an undefined validation template */ private final static String UNDEFINED_VALIDATION_TEMPLATE = "Undefined Validation Template"; /** * Sets the default factory to be used for converting * model thrown exceptions. * @param aFactory new factory */ public static void setDefaultFactory(ERXValidationFactory aFactory) { _defaultFactory = aFactory; } /** * Returns the default factory. If one has not * been set then a factory is created of type * ERXValidationFactory. * @return the default validation factory */ public static ERXValidationFactory defaultFactory() { if (_defaultFactory == null) setDefaultFactory(new ERXValidationFactory()); return _defaultFactory; } /** * Returns the default validation delegate that will * be set on all validation exceptions created. At the * moment delegates should implement the ExceptionDelegateInterface. * This will change to an informal implementation soon. * @return the default validation exception delegate. */ public static Object defaultDelegate() { return _defaultValidationDelegate; } /** * Sets the default validation delegate that * will be set on all validation exceptions that * are created by the factory. At the moment the * delegate set needs to implement the interface * ExceptionDelegateInterface. * @param obj default validation delegate */ public static void setDefaultDelegate(Object obj) { _defaultValidationDelegate = obj; } /** * The validation factory interface. This interface * is currently not being used. */ public interface FactoryInterface { public Class validationExceptionClass(); public void setValidationExceptionClass(Class class1); public ERXValidationException createException(EOEnterpriseObject eo, String property, Object value, String type); public ERXValidationException createCustomException(EOEnterpriseObject eo, String method); } /** * Exception delegates can be used to provide hooks to customize * how messages are generated for validation exceptions and how * templates are looked up. A validation exception can have a * delegate set or a default delegate can be set on the factory * itself. */ public interface ExceptionDelegateInterface { public String messageForException(ERXValidationException erv); public String templateForException(ERXValidationException erv); public NSKeyValueCoding contextForException(ERXValidationException erv); } /** * In the static initializer the mapping dictionary is * created. */ static { String keys[] = { // MESSAGE LIST: "to be null", // "The 'xxxxx' property is not allowed to be NULL" "Invalid Number", // "Invalid Number" "must have a ", // "The owner property of Bug must have a People assigned " "must have at least one", // "The exercises property of ERPCompanyRole must have at least one ERPExercise" "relationship, there is a related object", // "Removal of ERPAccount object denied because its children relationship is not empty" "relationship, there are related objects", // "Removal of ERPAccount object denied because its children relationship is not empty" "exceeds maximum length of", "Error encountered converting value of class" }; String objects[] = { ERXValidationException.NullPropertyException, ERXValidationException.InvalidNumberException, ERXValidationException.MandatoryToOneRelationshipException, ERXValidationException.MandatoryToManyRelationshipException, ERXValidationException.ObjectRemovalException, ERXValidationException.ObjectsRemovalException, ERXValidationException.ExceedsMaximumLengthException, ERXValidationException.ValueConversionException }; _mappings = new NSDictionary<>(objects, keys); } /** holds the validation exception class */ private Class _validationExceptionClass; /** holds the template cache for a given set of keys */ private Map<ERXMultiKey, String> _cache = new Hashtable<>(1000); /** holds the default template delimiter, "@@" */ private String _delimiter = "@@"; /** caches the constructor used to build validation exceptions */ protected Constructor regularConstructor; /** * Sets the validation class to be used when * creating validation exceptions. * @param class1 validation exception class */ public void setValidationExceptionClass(Class class1) { _validationExceptionClass = class1; } /** * Returns the validation exception class to use * when creating exceptions. If none is specified * {@link ERXValidationException} is used. * @return class object of validation exceptions to * be used. */ public Class validationExceptionClass() { if (_validationExceptionClass == null) _validationExceptionClass = ERXValidationException.class; return _validationExceptionClass; } /** * Simple method used to lookup and cache the * constructor to build validation exceptions. * @return constructor used to build validation exceptions */ protected Constructor regularValidationExceptionConstructor() { if (regularConstructor == null) { try { regularConstructor = validationExceptionClass().getConstructor(_regularConstructor); } catch (Exception e) { log.error("Exception looking up regular constructor.", e); } } return regularConstructor; } /** * Entry point for creating validation exceptions. This * method is used by all of the other methods to create * validation exceptions for an enterprise object, a property * key, a value and a type. The type should correspond to * one of the validation exception types defined in * {@link ERXValidationException ERXValidationException}. * @param eo enterprise object that is failing validation * @param property attribute that failed validation * @param value that failed validating * @param type of the validation exception * @return validation exception for the given information */ public ERXValidationException createException(EOEnterpriseObject eo, String property, Object value, String type) { ERXValidationException erve = null; try { log.debug("Creating exception for type: {} validationExceptionClass: {}", type, validationExceptionClass()); erve = (ERXValidationException)regularValidationExceptionConstructor().newInstance(new Object[] {type, eo, property, value}); } catch (InvocationTargetException ite) { log.error("Caught InvocationTargetException creating regular validation exception: {}", ite.getTargetException()); } catch (Exception e) { log.error("Caught exception creating regular validation exception.", e); } return erve; } /** * Decides if an existing {@link ERXValidationException ERXValidationException} * should be re-created. This is useful if you have several subclasses of * exceptions for different types of objects or messages and the framework can * only convert to the base type given the information it has at that point. * @param erv previous validation exception * @param value value that failed validating * @return <code>true</code> if the exception should be recreated */ public boolean shouldRecreateException(ERXValidationException erv, Object value) { return false; } /** * Creates a custom validation exception for a given * enterprise object and method. This method is just * a cover method for calling the four argument method * specifying <code>null</code> for property and value. * @param eo enterprise object failing validation * @param method name of the method to use to look up the validation * exception template, for instance "FirstNameCanNotMatchLastNameValidationException" * @return a custom validation exception for the given criteria */ public ERXValidationException createCustomException(EOEnterpriseObject eo, String method) { return createCustomException(eo, null, null, method); } /** * Creates a custom validation exception. This is the preferred * way of creating custom validation exceptions. * @param eo enterprise object failing validation * @param property attribute that failed validation * @param value that failed validation * @param method unique identified usually corresponding to a * method name to pick up the validation template * @return custom validation exception */ public ERXValidationException createCustomException(EOEnterpriseObject eo, String property, Object value, String method) { ERXValidationException erv = createException(eo, property, value, ERXValidationException.CustomMethodException); if (erv != null) erv.setMethod(method); return erv; } /** * Converts a model thrown validation exception into * an {@link ERXValidationException ERXValidationException}. * This is a cover method for the two argument version * passing in null as the value. * @param eov validation exception to be converted * @return converted validation exception */ public ERXValidationException convertException(ValidationException eov) { return convertException(eov, null); } /** * Converts a given model thrown validation exception into * an {@link ERXValidationException ERXValidationException}. * This method is used by {@link ERXEntityClassDescription ERXEntityClassDescription} * to convert model thrown validation exceptions. This isn't * a very elegant solution, but until we can register our * our validation exception class this is what we have to do. * @param eov validation exception to be converted * @param value that failed validation * @return converted validation exception */ public ERXValidationException convertException(ValidationException eov, Object value) { ERXValidationException erve = null; log.debug("Converting exception: {} value: {}", eov, (value != null ? value : "<NULL>")); if (!(eov instanceof ERXValidationException)) { String message = eov.getMessage(); Object o = eov.object(); EOEnterpriseObject eo = ((o instanceof EOEnterpriseObject) ? (EOEnterpriseObject) o: null); //NSDictionary userInfo = eov.userInfo() != null ? (NSDictionary)eov.userInfo() : NSDictionary.EmptyDictionary; for (String key : _mappings.allKeys()) { //EOEnterpriseObject eo = (EOEnterpriseObject)userInfo.objectForKey(ValidationException.ValidatedObjectUserInfoKey); String type = _mappings.objectForKey(key); if (message.lastIndexOf(key) >= 0) { String property = eov.key(); if(property == null && message.indexOf("Removal") == 0) { //FIXME: (ak) pattern matching? property = NSArray.componentsSeparatedByString(message, "'").objectAtIndex(3); } if(property == null && message.indexOf("Error encountered converting") == 0) { //FIXME: (ak) pattern matching? property = NSArray.componentsSeparatedByString(message, "'").objectAtIndex(1); } erve = createException(eo, property, value, type); break; } } } else { ERXValidationException original = (ERXValidationException)eov; if(shouldRecreateException(original, value)) { erve = createException(original.eoObject(), original.key(), original.value(), original.type()); log.debug("Converting exception: {} value: {}", original, (original.value() != null ? original.value() : "<NULL>")); } else { erve = original; } } if (erve == null) { log.error("Unable to convert validation exception.", eov); } else { NSArray erveAdditionalExceptions = convertAdditionalExceptions(eov); if (erveAdditionalExceptions.count() > 0) erve.setAdditionalExceptions(erveAdditionalExceptions); if(erve != eov) { erve.setStackTrace(eov.getStackTrace()); } } return erve; } /** * Converts the additional exceptions contained in an Exception to ERXValidationException subclasses. * @param ex validation exception * @return NSArray of converted exceptions */ protected NSArray<ERXValidationException> convertAdditionalExceptions(ValidationException ex) { NSArray<ValidationException> additionalExceptions = ex.additionalExceptions(); if (additionalExceptions == null || additionalExceptions.isEmpty()) { return NSArray.EmptyArray; } NSMutableArray<ERXValidationException> erveAdditionalExceptions = new NSMutableArray<>(); for (ValidationException e : additionalExceptions) { ERXValidationException erve = convertException(e); if (erve != null) erveAdditionalExceptions.addObject(erve); } return erveAdditionalExceptions; } /** * Entry point for generating an exception message * for a given message. The method <code>getMessage</code> * off of {@link ERXValidationException ERXValidationException} * calls this method passing in itself as the parameter. * @param erv validation exception * @return a localized validation message for the given exception */ // FIXME: Right now the delegate methods are implemented as a formal interface. Not ideal. Should be implemented as // an informal interface. Can still return null to not have an effect. public String messageForException(ERXValidationException erv) { String message = null; if (erv.delegate() != null && erv.delegate() instanceof ExceptionDelegateInterface) { message = ((ExceptionDelegateInterface)erv.delegate()).messageForException(erv); } if (message == null) { Object context = erv.context(); // AK: as the exception doesn't have a very special idea in how the message should get // formatted when gets displayed, we ask the context *first* before asking the exception. String template = templateForException(erv); if(template.startsWith(UNDEFINED_VALIDATION_TEMPLATE)) { // try to get the actual exception message if one is set message = erv._getMessage(); if(message == null) { message = template; } } else { if(context == erv || context == null) { message = ERXSimpleTemplateParser.sharedInstance().parseTemplateWithObject( template, templateDelimiter(), erv); } else { message = ERXSimpleTemplateParser.sharedInstance().parseTemplateWithObject( template, templateDelimiter(), context, erv); } } } return message; } /** * Entry point for finding a template for a given validation * exception. Override this method to provide your own * template resolution scheme. * @param erv validation exception * @return validation template for the given exception */ public String templateForException(ERXValidationException erv) { String template = null; if (erv.delegate() != null && erv.delegate() instanceof ExceptionDelegateInterface) { template = ((ExceptionDelegateInterface)erv.delegate()).templateForException(erv); } if (template == null) { String entityName = erv.eoObject() == null ? null : erv.eoObject().entityName(); String property = erv.isCustomMethodException() ? erv.method() : erv.propertyKey(); String type = erv.type(); String targetLanguage = erv.targetLanguage(); if (targetLanguage == null) { targetLanguage = ERXLocalizer.currentLocalizer() != null ? ERXLocalizer.currentLocalizer().language() : ERXLocalizer.defaultLanguage(); } log.debug("templateForException with entityName: {}; property: {}; type: {}; targetLanguage: {}", entityName, property, type, targetLanguage); ERXMultiKey k = new ERXMultiKey (new Object[] {entityName, property, type,targetLanguage}); template = _cache.get(k); // Not in the cache. Simple resolving. if (template == null) { template = templateForEntityPropertyType(entityName, property, type, targetLanguage); _cache.put(k, template); } } return template; } /** * Called when the Localizer is reset. This will * reset the template cache. * @param n notification posted when the localizer * is reset. */ public void resetTemplateCache(NSNotification n) { _cache = new Hashtable<>(1000); log.debug("Resetting template cache"); } /** * The context for a given validation exception can be used * to resolve keys in validation template. If a context is * not provided for a validation exception then this method * will be called if a context is needed for a validation * exception. Override this method if you want to provide * your own default contexts to validation exception template * parsing. * @param erv a given validation exception * @return context to be used for this validation exception */ // CHECKME: Doesn't need to be the NSKeyValueCoding interface now with WO 5 public NSKeyValueCoding contextForException(ERXValidationException erv) { NSKeyValueCoding context = null; if (erv.delegate() != null && erv.delegate() instanceof ExceptionDelegateInterface) { context = ((ExceptionDelegateInterface)erv.delegate()).contextForException(erv); } return context; } /** * Returns the template delimiter, the * default delimiter is "@@". * @return template delimiter */ public String templateDelimiter() { return _delimiter; } /** * Sets the template delimiter to be used * when parsing templates for creating validation * exception messages. * @param delimiter to be set */ public void setTemplateDelimiter(String delimiter) { _delimiter = delimiter; } /** * Method used to configure the validation factory * for operation. This method is called on the default * factory from an observer when the application is * finished launching. */ public void configureFactory() { // CHECKME: This might be better configured in a static init block of ERXValidationFactory. ERXValidation.setPushChangesDefault(ERXValueUtilities.booleanValueWithDefault(ERXSystem.getProperty("er.extensions.ERXValidationShouldPushChangesToObject"), ERXValidation.DO_NOT_PUSH_INCORRECT_VALUE_ON_EO)); if (WOApplication.application()!=null && !WOApplication.application().isCachingEnabled()) { NSNotificationCenter center = NSNotificationCenter.defaultCenter(); center.addObserver(this, new NSSelector("resetTemplateCache", ERXConstant.NotificationClassArray), ERXLocalizer.LocalizationDidResetNotification, null); } } /** * Finds a template for a given entity, property key, exception type and target * language. This method provides the defaulting behaviour needed to handle model * thrown validation exceptions. * @param entityName name of the entity * @param property key name * @param type validation exception type * @param targetLanguage target language name * @return a template for the given set of parameters */ protected String templateForEntityPropertyType(String entityName, String property, String type, String targetLanguage) { log.debug("Looking up template for entity named '{}' property '{}' type '{}' target language '{}'.", entityName, property, type, targetLanguage); // 1st try the whole string. String template = templateForKeyPath(entityName + "." + property + "." + type, targetLanguage); // 2nd try everything minus the type. if (template == null) template = templateForKeyPath(entityName + "." + property, targetLanguage); // 2.5th try entity plus type if (template == null) template = templateForKeyPath(entityName + "." + type, targetLanguage); // 3rd try property plus type if (template == null) template = templateForKeyPath(property + "." + type, targetLanguage); // 4th try just property if (template == null) template = templateForKeyPath(property, targetLanguage); // 5th try just type if (template == null) template = templateForKeyPath(type, targetLanguage); if (template == null) { template = UNDEFINED_VALIDATION_TEMPLATE + " entity \"" + entityName + "\" property \"" + property + "\" type \"" + type + "\" target language \"" + targetLanguage + "\""; log.error(template, new Throwable()); } return template; } /** * Get the template for a given key in a given language. * Uses {@link ERXLocalizer} to handle the actual lookup. * @param key the key to lookup * @param language use localizer for this language * @return template for key or <code>null</code> if none is found */ public String templateForKeyPath(String key, String language) { return (String)ERXLocalizer.localizerForLanguage(language).valueForKey(VALIDATION_TEMPLATE_PREFIX + key); } }