/*
* 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.eof;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOEntityClassDescription;
import com.webobjects.eoaccess.EOModel;
import com.webobjects.eoaccess.EOModelGroup;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eocontrol.EOClassDescription;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOGenericRecord;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.eocontrol.EOKeyGlobalID;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOQualifierEvaluation;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSNotification;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation.NSPropertyListSerialization;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation.NSTimestamp;
import com.webobjects.foundation.NSValidation;
import er.extensions.foundation.ERXFileNotificationCenter;
import er.extensions.foundation.ERXFileUtilities;
import er.extensions.foundation.ERXMutableDictionary;
import er.extensions.foundation.ERXPatcher;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.foundation.ERXThreadStorage;
import er.extensions.foundation.ERXTimestampUtilities;
import er.extensions.foundation.ERXValueUtilities;
import er.extensions.localization.ERXLocalizer;
import er.extensions.partials.ERXPartial;
import er.extensions.validation.ERXValidationException;
import er.extensions.validation.ERXValidationFactory;
/**
* The main purpose of the ERXClassDescription class is
* to throw {@link ERXValidationException}s instead of the
* usual {@link com.webobjects.foundation.NSValidation.ValidationException
* NSValidation.ValidationException} objects. See the
* ERXValidationException and ERXValidationFactory class
* for more information about localized and templatized
* validation exceptions. This class is configured to
* register itself as the class description by calling
* the method <code>registerDescription</code>. This method
* is called when the principal class of this framework is
* loaded. This happens really early so you shouldn't have
* to worry about this at all.
* <p>
* Additionally, this class allows for model driven validations in a "poor-mans-Validity-way":
* add a <code>ERXValidation</code> user info entry on your entity.
* This is an example:<pre><code>
* "ERXValidation" = {
* // these keys are evaluated on validateForSave, they don't correspond to properties
* additionalValidationKeys = ("validateEmailPassword");
*
* // This dictionary holds the keys to use for validating properties
* validateForKey =
* {
*
* // these keys are evaluated on validateForSave, they don't correspond to properties
* email =
* (
* {
* // this is the message code into ValidationStrings.plist
* // User.email.wrongLength = "The mail does not have the right size (5 to 50)";
* message = "wrongLength";
*
* // skip this rule if the value is null
* ignoreIfNull = true;
*
* // if there is a qualifier key, then a dictionary containing "object" and "value" is evaluated
* // and an exception is thrown if the evaluation returns false
* qualifier = "(value.length >= 5) AND (value.length < 50)";
* },
* {
* // again, this is the message code into ValidationStrings.plist
* message = "sampleTest";
*
* // Given this key, an object of the corresponding EOQualifierEvaluation subclass is created
* // and given this dictionary on creation. This object needs to be re-entrant.
* className = "SampleTest";
* // an example is:
* // public class SampleTest implements EOQualifierEvaluation {
* // int minLength, maxLength;
* // public SampleTest(Object param) {
* // NSDictionary dict = (NSDictionary)param;
* // minLength = ERXValueUtilities.intValue(dict.objectForKey("minLength"));
* // maxLength = ERXValueUtilities.intValue(dict.objectForKey("maxLength"));
* // }
* // public boolean evaluateObject(Object o) {
* // ERXEntityClassDescription.ValidationObjectValue val
* // = (ERXEntityClassDescription.ValidationObjectValue)o;
* // EOEnterpriseObject eo = val.object();
* // String value = (String)val.value();
* // return value.length() >= minLength && value.length() <= maxLength;
* // }
* // }
*
* minLength = "5";
* maxLength = "10";
* }
* );
*
* // This key does not correspond to any property, it get's evaluated in D2WApps where you have a
* // multi-step page and need to do validation before validateForSave
* "validateEmailPassword" =
* (
* {
* message = "stupidTestWithEmailAndPassword";
*
* // means to get D2W to highlight the fields involved instead of only displaying the message
* // For this to work, your corresponding localized String should be
* // User.email,password.stupidTestWithEmailAndPassword = "Stupid test failed";
* keyPaths = "email,password";
*
* qualifier = "(object.email.length >= object.password.length)";
* }
* );
* };
*
* // These get checked when the object gets saved, additionally to "additionalValidations"
* // The structure of "validateForInsert", "validateForUpdate" and "validateForDelete" is the same.
* validateForSave =
* (
* {
* message = "cantBeBoth";
*
* keyPaths = "isEditor,isAdmin";
*
* qualifier = "(object.isEditor = 'Y' and object.isAdmin = 'Y')";
* }
* );
* }</code></pre>
* If you have validation methods in your EO classes (e.g. <code>validateName(String name)</code> for attribute <i>name</i>)
* be aware that those are executed first and that they possibly coerce the value to validate (e.g. making a string uppercase).
* The validations done by this class will be executed on those potentially coerced values.
* <p>
* This code is mainly a quick-and-dirty rewrite from PRValidation by Proteon.
* <p>
* Additionally, this class adds a concept of "Default" values that get pushed into the object at creation time.
* Simply add a "ERXDefaultValues" key into the entity's userInfo dictionary that contains key-value-pairs for every default you want to set. Alternately, you can set a "default" key on each of the relationship or attribute's userInfo.
* <h3>Example:</h3>
* <pre><code>
* "ERXDefaultValues" = {
*
* // Example for simple values.
* isAdmin = N;
* isEditor = Y;
*
* // Example for a related object (->Languages(pk,displayName)). You need to enter the primary key value.
* language = "de";
*
* // Example for an NSArray of related objects
* recommendingUser = "@threadStorage.actor";
*
* // Example for an NSArray
* articlesToRevisit = "@threadStorage.actor.articles";
*
* // Example for a NSTimestamp. All static methods from ERXTimestampUtilities are supported.
* created = "@now";
* updatePassword = "@tomorrow";
*
* }</code></pre>
* If you wish to provide your own class description subclass
* see the documentation associated with the Factory inner class.
*/
public class ERXEntityClassDescription extends EOEntityClassDescription {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(ERXEntityClassDescription.class);
/** validation logging support */
private static final Logger validationLog = LoggerFactory.getLogger("er.validation.ERXEntityClassDescription");
/** default logging support */
public static final Logger defaultLog = LoggerFactory.getLogger("er.default.ERXEntityClassDescription");
/** Holds validation info from the entities user info dictionary */
protected NSDictionary _validationInfo;
/** Holds validation qualifiers */
protected NSMutableDictionary _validationQualiferCache;
/** Holds default values */
protected NSMutableDictionary _initialDefaultValues;
/** Holds the default factory instance */
private static Factory _factory;
/** holds validity Methods */
private static Method[] validityMethods = null;
/** index of validity save method */
private static int VALIDITY_SAVE = 0;
/** index of validity delete method */
private static int VALIDITY_DELETE = 1;
/** index of validity insert method */
private static int VALIDITY_INSERT = 2;
/** index of validity update method */
private static int VALIDITY_UPDATE = 3;
/** the shared validity engine instance as Object to eliminate compile errors
* if validity is not linked and should not be used
*/
private static Object sharedGSVEngineInstance;
/** Boolean that gets initialized on first use to indicate if validity should
* be used or not, remember that the call System.getProperty acts synchronized
* so this saves some time in multithreaded apps.
*/
private static Boolean useValidity;
public static final String ValidateEntityClassAvailability = "ERXEntityClassDescription.validateEntityClassAvailability";
/**
* This factory inner class is registered as the observer
* for three notifications: modelWasAdded, classDescriptionNeededForEntity
* and classDescriptionNeededForClass. If you wish to provide your own
* subclass of ERXEntityClassDescription then you need to create a
* subclass of Factory and set that class name in the system properties
* under the key: <code>er.extensions.ERXClassDescription.factoryClass</code>
* In your Factory subclass override the method: newClassDescriptionForEntity
* to provide your own ERXEntityClassDescription subclass.
*/
public static class Factory {
/** Public constructor */
public Factory() {
// Need to be able to preempt the model registering descriptions.
NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("modelWasAdded", ERXConstant.NotificationClassArray), EOModelGroup.ModelAddedNotification, null);
NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("modelGroupWasAdded", ERXConstant.NotificationClassArray), ERXModelGroup.ModelGroupAddedNotification, null);
NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("classDescriptionNeededForEntityName", ERXConstant.NotificationClassArray), EOClassDescription.ClassDescriptionNeededForEntityNameNotification, null);
NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("classDescriptionNeededForClass", ERXConstant.NotificationClassArray), EOClassDescription.ClassDescriptionNeededForClassNotification, null);
}
public void reset() {
_registeredModelNames = new NSMutableArray();
_entitiesForClass = new NSMutableDictionary();
_classDescriptionForEntity = new NSMutableDictionary();
}
protected boolean isRapidTurnaroundEnabled() {
return ERXProperties.booleanForKey("er.extensions.ERXEntityClassDescription.isRapidTurnaroundEnabled");
}
protected boolean isFixingRelationshipsEnabled() {
return ERXProperties.booleanForKey("er.extensions.ERXEntityClassDescription.isFixingRelationshipsEnabled");
}
/**
* Method called when a model group did load.
*/
public final void modelGroupWasAdded(NSNotification n) {
log.debug("modelGroupWasAdded: {}", n);
EOModelGroup group = (EOModelGroup) n.object();
processModelGroup(group);
}
/**
* Called when a model group finished loading. Checks foreign keys by default. Override to to more...
* @param group
*/
protected void processModelGroup(EOModelGroup group) {
for (Enumeration ge = group.models().objectEnumerator(); ge.hasMoreElements();) {
EOModel model = (EOModel)ge.nextElement();
String frameworkName = null;
String modelPath = null;
log.debug("ApplicationDidFinishLaunching: {}", model.name());
if(isRapidTurnaroundEnabled()) {
for(Enumeration e = NSArray.componentsSeparatedByString(model.pathURL().getFile(), File.separator).reverseObjectEnumerator(); e.hasMoreElements(); ) {
String a = (String)e.nextElement();
if(a.indexOf(".framework") > 0) {
frameworkName = a.substring(0, a.indexOf(".framework"));
break;
}
}
if(frameworkName == null) {
frameworkName = "app";
}
modelPath = ERXFileUtilities.pathForResourceNamed(model.name() + ".eomodeld", frameworkName, null);
defaultLog.debug("Path for model <{}> in framework <{}>: {}", model.name(), frameworkName, modelPath);
}
for (Enumeration ee = model.entities().objectEnumerator(); ee.hasMoreElements();) {
EOEntity entity = (EOEntity)ee.nextElement();
checkForeignKeys(entity);
EOClassDescription cd = EOClassDescription.classDescriptionForEntityName(entity.name());
defaultLog.debug("Reading defaults for: {}", entity.name());
if(cd instanceof ERXEntityClassDescription) {
((ERXEntityClassDescription)cd).readDefaultValues();
if(isRapidTurnaroundEnabled() && modelPath != null) {
String path = modelPath + File.separator + entity.name() + ".plist";
ERXFileNotificationCenter.defaultCenter().addObserver(cd, new NSSelector("modelFileDidChange", ERXConstant.NotificationClassArray), path);
}
} else {
defaultLog.warn("Entity classDescription is not ERXEntityClassDescription: {}", entity.name());
}
}
}
}
/**
* Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
* when an EOModel is loaded.
* This method just calls the method
* <code>registerDescriptionForEntitiesInModel</code>
*
* @param n notification that has the EOModel that was loaded.
*/
public final void modelWasAdded(NSNotification n) {
EOModel model = ((EOModel)n.object());
log.debug("ModelWasAddedNotification: {}", model.name());
// Don't want this guy getting in our way.
NSNotificationCenter.defaultCenter().removeObserver(model);
try {
registerDescriptionForEntitiesInModel(model);
} catch (RuntimeException e) {
log.error("Error registering model: {}", model.name(), e);
throw e;
}
}
/**
* Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
* when a class description is needed
* for a given entity. Usually this method isn't needed seeing
* as we preempt the on demand loading of class descriptions
* by loading all of them when the EOModel is loaded.
* This method just calls the method
* <code>registerDescriptionForEntity</code>
*
* @param n notification that has the name of the entity
* that needs the class description.
*/
public void classDescriptionNeededForEntityName(NSNotification n) {
log.debug("classDescriptionNeededForEntityName: {}", n.object());
String name = (String)n.object();
EOEntity e = ERXEOAccessUtilities.entityNamed(null,name);
if(e == null) log.error("Entity '{}' not found in the default model group!", name);
if (e != null) {
registerDescriptionForEntity(e);
}
}
/**
* Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
* when a class description is needed
* for a given Class. Usually this method isn't needed seeing
* as we preempt the on demand loading of class descriptions
* by loading all of them when the EOModel is loaded.
* This method just calls the method
* <code>registerDescriptionForClass</code>
* @param n notification that has the Class object
* that needs a class description.
*/
public void classDescriptionNeededForClass(NSNotification n) {
Class c = (Class)n.object();
log.debug("classDescriptionNeededForClass: {}", c);
registerDescriptionForClass(c);
}
/**
* Factory method that is used to create a new class
* description for a given entity. Sub classes that
* wish to provide a sub class of ERXEntityClassDescription
* should override this method to create that custom
* description. By default this method returns a new
* ERXEntityClassDescription.
* @param entity to create the class description for
* @return new class description for the given entity
*/
protected ERXEntityClassDescription newClassDescriptionForEntity(EOEntity entity) {
String key = entity.name();
EOModel model = entity.model();
if (model != null) {
key = model.name() + " " + key;
}
ERXEntityClassDescription classDescription = (ERXEntityClassDescription)_classDescriptionForEntity.objectForKey(key);
if (classDescription == null) {
classDescription = new ERXEntityClassDescription(entity);
_classDescriptionForEntity.setObjectForKey(classDescription, key);
}
return classDescription;
}
/** holds a reference to all of the registered model names */
private NSMutableArray _registeredModelNames = new NSMutableArray();
/** holds a mapping of class to entities */
private NSMutableDictionary _entitiesForClass = new NSMutableDictionary();
/** holds a mapping of entity to class descriptions */
private NSMutableDictionary _classDescriptionForEntity = new NSMutableDictionary();
/**
* Allows for entities to be altered
* before they have a custom class description
* registered. Sub classes can override this method
* to provide any extra alterings before the description
* is registered. However be sure to call super as this
* method does convert the class name from EOGenericRecord
* to ERXGenericRecord, which unfortunately is required
* for custom validation to work at the moment.
* @param eoentity to be prepared for registration
*/
protected void prepareEntityForRegistration(EOEntity eoentity) {
String className = eoentity.className();
String defaultClassName = ERXProperties.stringForKeyWithDefault("er.extensions.ERXEntityClassDescription.defaultClassName", ERXGenericRecord.class.getName());
String alternateClassName = ERXProperties.stringForKey("er.extensions.ERXEntityClassDescription." + eoentity.name() + ".ClassName");
if (alternateClassName != null) {
log.debug("{}: setting class from: {} to: {}", eoentity.name(), className, alternateClassName);
eoentity.setClassName(alternateClassName);
} else if (className.equals("EOGenericRecord")) {
eoentity.setClassName(defaultClassName);
}
}
/**
* Handles errors when an optional relationship has a source attribute
* that is set to allow null values. Subclasses can override this to do more specific handling.
*/
protected void handleOptionalRelationshipError(EOEntity eoentity, EORelationship relationship, EOAttribute attribute) {
if(isFixingRelationshipsEnabled()) {
relationship.setIsMandatory(true);
log.info("{}: relationship '{}' was switched to mandatory, because the foreign key '{}' does NOT allow NULL values",
eoentity.name(), relationship.name(), attribute.name());
} else {
log.warn("{}: relationship '{}' is marked to-one and optional, but the foreign key '{}' does NOT allow NULL values",
eoentity.name(), relationship.name(), attribute.name());
}
}
/**
* Handles errors when a mandatory relationship has a source attribute
* that is set to not allow null values. Subclasses can override this to do more specific handling.
*/
protected void handleMandatoryRelationshipError(EOEntity eoentity, EORelationship relationship, EOAttribute attribute) {
if(isFixingRelationshipsEnabled()) {
relationship.setIsMandatory(false);
log.info("{}: relationship '{}' was switched to optional, because the foreign key '{}' allows NULL values",
eoentity.name(), relationship.name(), attribute.name());
} else {
log.warn("{}: relationship '{}' is marked to-one and mandatory, but the foreign key '{}' allows NULL values",
eoentity.name(), relationship.name(), attribute.name());
}
}
/**
* Checks for foreign keys that are <code>NOT NULL</code>,
* but whose relationship is marked as non-mandatory and vice-versa. This
* error is not checked by EOModeler, so we do it here.
* @param eoentity to be check
*/
public void checkForeignKeys(EOEntity eoentity) {
NSArray primaryKeys = eoentity.primaryKeyAttributes();
for(Enumeration relationships = eoentity.relationships().objectEnumerator(); relationships.hasMoreElements(); ) {
EORelationship relationship = (EORelationship)relationships.nextElement();
if(!relationship.isToMany()) {
if(relationship.isMandatory()) {
for(Enumeration attributes = relationship.sourceAttributes().objectEnumerator(); attributes.hasMoreElements(); ) {
EOAttribute attribute = (EOAttribute)attributes.nextElement();
if(attribute.allowsNull()) {
handleMandatoryRelationshipError(eoentity, relationship, attribute);
}
}
} else {
for(Enumeration attributes = relationship.sourceAttributes().objectEnumerator(); attributes.hasMoreElements(); ) {
EOAttribute attribute = (EOAttribute)attributes.nextElement();
if(!attribute.allowsNull() && !primaryKeys.containsObject(attribute)) {
handleOptionalRelationshipError(eoentity, relationship, attribute);
}
}
}
}
}
}
/**
* This method registers custom class descriptions for all
* of the entities in a given model. This method is called
* when a model is loaded. The reason for this method is
* to preempt the usual class description loading mechanism
* which has a race condition involved for the order in
* which the notifications are recieved.
* @param model that contains all of the entities to be registerd
*/
protected void registerDescriptionForEntitiesInModel(EOModel model) {
if (!_registeredModelNames.containsObject(model.name())) {
for (Enumeration e = model.entities().objectEnumerator(); e.hasMoreElements();) {
EOEntity eoentity = (EOEntity)e.nextElement();
String className = eoentity.className();
prepareEntityForRegistration(eoentity);
NSMutableArray array = (NSMutableArray)_entitiesForClass.objectForKey(className);
if(array == null) {
array = new NSMutableArray();
}
if (log.isDebugEnabled())
log.debug("Adding entity {} with class {}", eoentity.name(), eoentity.className());
array.addObject(eoentity);
_entitiesForClass.setObjectForKey(array, eoentity.className());
//HACK ALERT: (ak) We work around classDescriptionForNewInstances() of EOEntity being broken here...
registerDescriptionForEntity(eoentity);
}
_registeredModelNames.addObject(model.name());
}
// Don't want this guy getting in our way later on ;
NSNotificationCenter.defaultCenter().removeObserver(model);
}
/**
* This is a hack to work around RadarBug:2867501. EOEntity
* is hardwired to return an EOEntityClassdescription for the
* method classDescriptionForNewInstances, this causes a serious
* problem when using custom class descriptions with D2W which
* makes use of this method. What this hack does is use the magic
* of key-value coding to push our custom class description onto
* a given entity. In order to do this we needed to add the
* custom {@link KVCProtectedAccessor} to the package
* com.webobjects.eoaccess.
* @param entity to have the custom class description set on
* @param cd class description to set on the entity
*/
private void _setClassDescriptionOnEntity(EOEntity entity, ERXEntityClassDescription cd) {
try {
//HACK ALERT: (ak) We push the cd rather rudely into the entity to have it ready when classDescriptionForNewInstances() is called on it. We will have to add a com.webobjects.eoaccess.KVCProtectedAccessor to make this work
NSKeyValueCoding.Utility.takeValueForKey(entity, cd, "classDescription");
} catch(RuntimeException ex) {
log.warn("_setClassDescriptionOnEntity", ex);
}
}
/**
* Registers a custom class description for the given
* entity using the method <code>newClassDescriptionForEntity</code>
* which can be overridden by subclasses to provide a
* different class description subclass.
* @param entity to register the class description for
*/
protected void registerDescriptionForEntity(EOEntity entity) {
Class entityClass = EOGenericRecord.class;
String className = entity.className();
if (log.isDebugEnabled()) {
log.debug("Registering description for entity: {} with class: {}", entity.name(), className);
}
if (ERXProperties.booleanForKeyWithDefault(ValidateEntityClassAvailability, true)) { // Make it possible to opt-out of this check.
try {
entityClass = className.endsWith("EOGenericRecord") ? EOGenericRecord.class : Class.forName(className);
} catch (java.lang.ClassNotFoundException ex) {
throw new RuntimeException("Invalid class name '" + className + "' for entity '" + entity.name() + "'." + (!className.contains(".") ? " (The class name should include the full package path of the class.)" : ""), ex);
}
}
ERXEntityClassDescription cd = newClassDescriptionForEntity(entity);
EOClassDescription.registerClassDescription(cd, entityClass);
_setClassDescriptionOnEntity(entity, cd);
}
/**
* This method is called when a class description is
* needed for a particular class. Here we use the
* previous cache that we constructed of class to
* entity map when the models were loaded. In this
* way we can register all of the custom class
* descriptions for a given class if need be.
* @param class1 class object to have a custom class
* description registered for.
*/
protected void registerDescriptionForClass(Class class1) {
NSArray entities = (NSArray)_entitiesForClass.objectForKey(class1.getName());
if (entities != null) {
if (log.isDebugEnabled())
log.debug("Registering descriptions for class: {} found entities: {}", class1, entities.valueForKey("name"));
for (Enumeration e = entities.objectEnumerator(); e.hasMoreElements();) {
EOEntity entity = (EOEntity)e.nextElement();
ERXEntityClassDescription cd = newClassDescriptionForEntity(entity);
EOClassDescription.registerClassDescription(cd, class1);
_setClassDescriptionOnEntity(entity, cd);
}
} else {
if(class1.getName().indexOf('$') < 0) {
log.error("Unable to register descriptions for class: {}", class1, new RuntimeException("Dummy"));
}
}
}
}
/** getter for the factory */
public static Factory factory() {
return _factory;
}
/**
* This method is called by the principal class
* of the framework when the framework's NSBundle is
* loaded. This method registers an observer, either
* a Factory object, which is an inner class of this class
* or a custom Factory subclass specified in the property:
* <b>er.extensions.ERXClassDescription.factoryClass</b>.
* This observer listens for notifications when a model
* is loaded or a class description is needed and responds
* by creating and registering custom class descriptions.
*/
public static void registerDescription() {
if (_factory == null) {
_factory = null;
try {
String className = ERXProperties.stringForKey("er.extensions.ERXClassDescription.factoryClass");
if (className != null) {
_factory = (Factory)Class.forName(className).newInstance();
}
} catch(Exception ex) {
log.warn("Exception while registering factory, using default.", ex);
}
if(_factory == null)
_factory=new Factory();
}
}
/**
* Public constructor
* @param entity that this class description corresponds to
*/
public ERXEntityClassDescription(EOEntity entity) {
super(entity);
_validationInfo = ERXValueUtilities.dictionaryValue(entity.userInfo().objectForKey("ERXValidation"));
_validationQualiferCache = ERXMutableDictionary.synchronizedDictionary();
}
public void modelFileDidChange(NSNotification n) {
File file = (File)n.object();
try {
defaultLog.debug("Reading .plist for entity <{}>", entity());
NSDictionary userInfo = (NSDictionary)NSPropertyListSerialization.propertyListFromString(ERXFileUtilities.stringFromFile(file));
entity().setUserInfo((NSDictionary)userInfo.objectForKey("userInfo"));
_validationInfo = ERXValueUtilities.dictionaryValue(entity().userInfo().objectForKey("ERXValidation"));
_validationQualiferCache = ERXMutableDictionary.synchronizedDictionary();
_initialDefaultValues = null;
readDefaultValues();
} catch(Exception ex) {
defaultLog.error("Can't read file <{}>", file, ex);
}
}
/**
* This method is called when an object is
* about to be deleted. If any validation
* exceptions occur they are converted to an
* {@link ERXValidationException} and that is
* thrown.
* @param obj enterprise object to be deleted
* @throws NSValidation.ValidationException validation exception
*/
@Override
public void validateObjectForDelete(EOEnterpriseObject obj) throws NSValidation.ValidationException {
try {
if (useValidity()) {
invokeValidityMethodWithType(VALIDITY_DELETE, obj);
}
super.validateObjectForDelete(obj);
validateObjectWithUserInfo(obj, null, "validateForDelete", "validateForDelete");
} catch (ERXValidationException eov) {
throw eov;
} catch (NSValidation.ValidationException eov) {
log.debug("Caught validation exception: {}", eov);
ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
throw (erv != null ? erv : eov);
}
}
/**
* Overridden to perform a check if the entity is still in a model group.
* This can happen if you remove the entity, clone it to change things and re-add it afterwards.
*/
@Override
public EOEntity entity() {
checkEntity();
return super.entity();
}
protected void checkEntity() {
if(_entity.model() == null) {
try {
EOEntity registeredEntity = ERXEOAccessUtilities.entityNamed(null,_entity.name());
if(registeredEntity != null) {
_entity = registeredEntity;
} else {
EOModel model = _entity.model();
if(model == null) {
model = ERXEOAccessUtilities.modelGroup(null).models().lastObject();
}
model.addEntity(_entity);
log.warn("Added <{}> to default model group.", _entity.name());
}
} catch (Exception ex) {
throw new RuntimeException("Model or modelgroup for <" + _entity.name() + "> is null: " + entity().model(), ex);
}
}
}
@Override
public EOEnterpriseObject createInstanceWithEditingContext(EOEditingContext ec, EOGlobalID gid) {
checkEntity();
return super.createInstanceWithEditingContext(ec, gid);
}
/**
* This method is called when an object is
* about to be updated. If any validation
* exceptions occur they are converted to an
* {@link ERXValidationException} and that is
* thrown.
* @param obj enterprise object to be deleted
* @throws NSValidation.ValidationException validation exception
*/
public void validateObjectForUpdate(EOEnterpriseObject obj) throws NSValidation.ValidationException {
try {
if (useValidity()) {
invokeValidityMethodWithType(VALIDITY_UPDATE, obj);
}
validateObjectWithUserInfo(obj, null, "validateForUpdate", "validateForUpdate");
} catch (ERXValidationException eov) {
throw eov;
} catch (NSValidation.ValidationException eov) {
log.debug("Caught validation exception: {}", eov);
ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
throw (erv != null ? erv : eov);
}
}
/**
* This method is called when an object is
* about to be inserted. If any validation
* exceptions occur they are converted to an
* {@link ERXValidationException} and that is
* thrown.
* @param obj enterprise object to be deleted
* @throws NSValidation.ValidationException validation exception
*/
public void validateObjectForInsert(EOEnterpriseObject obj) throws NSValidation.ValidationException {
try {
if (useValidity()) {
invokeValidityMethodWithType(VALIDITY_INSERT, obj);
}
validateObjectWithUserInfo(obj, null, "validateForInsert", "validateForInsert");
} catch (ERXValidationException eov) {
throw eov;
} catch (NSValidation.ValidationException eov) {
log.debug("Caught validation exception: {}", eov);
ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
throw (erv != null ? erv : eov);
}
}
/**
* This method is called to validate a value
* for a particular key. Typical validation
* exceptions that might occur are non-null
* constraints or string is greater in length
* than is allowed. If a validation
* exception does occur they are converted to an
* {@link ERXValidationException} and that is
* thrown.
* @param obj value to be validated
* @param s property key to validate the value
* against.
* @throws NSValidation.ValidationException validation exception
*/
@Override
public Object validateValueForKey(Object obj, String s) throws NSValidation.ValidationException {
Object validated = null;
log.debug("Validate value: {} for key: {}", obj, s);
try {
if(obj instanceof ERXConstant) {
validated = obj;
} else {
validated = super.validateValueForKey(obj, s);
}
} catch (ERXValidationException eov) {
throw eov;
} catch (NSValidation.ValidationException eov) {
log.debug("Caught validation exception: {}", eov);
ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
throw (erv != null ? erv : eov);
}
return validated;
}
/**
* This method is called when an object is
* about to be saved. Adds support for extra validation keys to
* be set in an array in the entity's userInfo under the keypath
* <code>ERXValidation.additionalValidationKeys</code>. If any validation
* exceptions occur they are converted to an
* {@link ERXValidationException} and that is
* thrown.
* @param obj enterprise object to be saved
* @throws NSValidation.ValidationException validation exception
*/
@Override
public void validateObjectForSave(EOEnterpriseObject obj) throws NSValidation.ValidationException {
try {
if (useValidity()) {
invokeValidityMethodWithType(VALIDITY_SAVE, obj);
}
if(_validationInfo != null) {
NSArray additionalValidationKeys = (NSArray)_validationInfo.objectForKey("additionalValidationKeys");
if(additionalValidationKeys != null) {
for(Enumeration e = additionalValidationKeys.objectEnumerator(); e.hasMoreElements();) {
String key = (String)e.nextElement();
NSSelector selector = new NSSelector(key);
if(selector.implementedByObject(obj)) {
try {
selector.invoke(obj);
} catch (Exception ex) {
if(ex instanceof NSValidation.ValidationException)
throw (NSValidation.ValidationException)ex;
log.error("Could not invoke {} on {}", key, obj, ex);
}
} else {
validateObjectWithUserInfo(obj, null, "validateForKey." + key, key);
}
}
}
}
validateObjectWithUserInfo(obj, null, "validateForSave", "validateForSave");
} catch (ERXValidationException eov) {
throw eov;
} catch (NSValidation.ValidationException eov) {
log.debug("Caught validation exception: {}", eov);
ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
throw (erv != null ? erv : eov);
}
}
public static class ValidationObjectValue {
protected EOEnterpriseObject object;
protected Object value;
public ValidationObjectValue(EOEnterpriseObject object, Object value) {
this.object = object;
this.value = value;
}
public Object value() { return value;}
public EOEnterpriseObject object() { return object;}
}
public static class QualiferValidation implements EOQualifierEvaluation {
protected EOQualifier qualifier;
public QualiferValidation(Object info) {
NSDictionary dict =(NSDictionary)info;
qualifier = EOQualifier.qualifierWithQualifierFormat((String)dict.objectForKey("qualifier"), null);
}
public boolean evaluateWithObject(Object o) {
return qualifier.evaluateWithObject(o);
}
}
protected boolean validateObjectValueDictWithInfo(ValidationObjectValue values, NSDictionary info, String cacheKey) {
EOQualifierEvaluation q = (EOQualifierEvaluation)_validationQualiferCache.objectForKey(cacheKey);
if(q == null) {
try {
String className = (String)info.objectForKey("className");
if(className == null) {
className = QualiferValidation.class.getName();
}
Class cl = ERXPatcher.classForName(className);
Constructor co = cl.getConstructor(new Class [] {Object.class});
Object o = co.newInstance(new Object[] {info});
q = (EOQualifierEvaluation)o;
} catch(Exception ex) {
throw new NSForwardException(ex);
}
_validationQualiferCache.setObjectForKey(q, cacheKey);
}
if(values.value() == null && "true".equals(info.objectForKey("ignoreIfNull")))
return true;
if(q != null)
return q.evaluateWithObject(values);
return true;
}
/**
* Validates a specific property of an EO by applying the rules found in the userInfo
* of the entity i.e. model driven validations. See class description for more info
* on it.
*
* @param object the EO validation is done for
* @param value the value to validate
* @param validationTypeString the key for the validation info from userInfo
* @param property the property key to validate
*/
public void validateObjectWithUserInfo(EOEnterpriseObject object, Object value, String validationTypeString, String property) {
if(_validationInfo != null) {
NSArray qualifiers = (NSArray)_validationInfo.valueForKeyPath(validationTypeString);
if(qualifiers != null) {
ValidationObjectValue values = new ValidationObjectValue(object, value);
int i = 0;
for(Enumeration e = qualifiers.objectEnumerator(); e.hasMoreElements();) {
NSDictionary info = (NSDictionary)e.nextElement();
if(validationLog.isDebugEnabled())
validationLog.debug("Validate {}.{} with <{}> on {}\nRule: {}", validationTypeString, property, value, object, info);
if(!validateObjectValueDictWithInfo(values, info, validationTypeString+property+i)) {
String message = (String)info.objectForKey("message");
String keyPaths = (String)info.objectForKey("keyPaths");
property = keyPaths == null ? property : keyPaths;
if(validationLog.isDebugEnabled())
validationLog.info("Validation failed {}.{} with <{}> on {}", validationTypeString, property, value, object);
throw ERXValidationFactory.defaultFactory().createException(object, property, value,message);
}
i = i+1;
}
}
}
}
/**
* Calculates a display name for a key using
* localization of entityname.key if found
* otherwise an improved method.
* @param key to be converted
* @return pretty display name
*/
@Override
public String displayNameForKey(String key) {
if (ERXLocalizer.isLocalizationEnabled()) {
return ERXLocalizer.currentLocalizer().localizedDisplayNameForKey(this, key);
}
return ERXStringUtilities.displayNameForKey(key);
}
/**** default handling */
// Default handling from here on
protected String defaultKey = "default";
public static interface Default {
public static final int AdaptorNumberType = 0;
public static final int AdaptorCharactersType = 1;
public static final int AdaptorBytesType = 2;
public static final int AdaptorDateType = 3;
public void setValueInObject(EOEnterpriseObject eo);
}
public static class AttributeDefault implements Default {
String key;
String stringValue;
int adaptorType;
EOAttribute attribute;
public AttributeDefault(EOAttribute attribute, String stringValue) {
this(attribute.name(), stringValue, attribute.adaptorValueType());
this.attribute = attribute;
}
public AttributeDefault(String key, String stringValue, int adaptorType) {
this.key = key;
this.stringValue = stringValue;
this.adaptorType = adaptorType;
}
public AttributeDefault(String key, String stringValue) {
this(key, stringValue, AdaptorCharactersType);
}
public void setValueInObject(EOEnterpriseObject eo) {
Object defaultValue = stringValue;
if(stringValue.startsWith("@threadStorage.")) {
String keyPath = stringValue.substring("@threadStorage.".length());
defaultValue = ERXThreadStorage.valueForKeyPath(keyPath);
} else {
if(attribute != null && attribute.valueFactoryMethodName() != null && attribute.factoryMethodArgumentType() == EOAttribute.FactoryMethodArgumentIsString) {
defaultValue = attribute.newValueForString(stringValue);
}
}
if(defaultValue != null) {
String s = defaultValue.toString();
s = s.substring(s.indexOf("@")+1);
if(adaptorType == AdaptorDateType) {
defaultValue = ERXTimestampUtilities.timestampForString(s);
} else if (adaptorType == AdaptorNumberType) {
NSTimestamp temp = ERXTimestampUtilities.timestampForString(s);
if(temp != null) {
defaultValue = ERXTimestampUtilities.unixTimestamp(temp);
} else {
//the value will be coerced by the eo...
defaultValue = ERXValueUtilities.bigDecimalValue(s);
}
}
}
eo.takeValueForKey(defaultValue, key);
}
}
public static class RelationshipDefault implements Default {
String key;
String stringValue;
int adaptorType;
String relationshipEntityName;
public RelationshipDefault(String key, String stringValue, int adaptorType, String relationshipEntityName) {
this.key = key;
this.stringValue = stringValue;
this.adaptorType = adaptorType;
this.relationshipEntityName = relationshipEntityName;
}
public void setValueInObject(EOEnterpriseObject eo) {
Object defaultValue = stringValue;
EOEditingContext ec = eo.editingContext();
if(stringValue.charAt(0) == '@') { // computed key
if(stringValue.equals("@new")) {
EOClassDescription cd = EOClassDescription.classDescriptionForEntityName(relationshipEntityName);
EOEnterpriseObject newObject = cd.createInstanceWithEditingContext(eo.editingContext(), null);
ec.insertObject(newObject);
eo.addObjectToBothSidesOfRelationshipWithKey(newObject,key);
} else if(stringValue.startsWith("@threadStorage.")) {
String keyPath = stringValue.substring("@threadStorage.".length());
Object o = ERXThreadStorage.valueForKey(keyPath);
if(keyPath.indexOf(".") > 0) {
keyPath = stringValue.substring(keyPath.indexOf(".")+1);
o = NSKeyValueCodingAdditions.Utility.valueForKeyPath(o, keyPath);
}
if(o != null) {
if(o instanceof EOEnterpriseObject) {
ERXEOControlUtilities.addObjectToObjectOnBothSidesOfRelationshipWithKey((EOEnterpriseObject)o, eo, key);
} else if(o instanceof NSArray) {
NSArray newObjects = (NSArray)o;
for(Enumeration e = newObjects.objectEnumerator(); e.hasMoreElements();) {
ERXEOControlUtilities.addObjectToObjectOnBothSidesOfRelationshipWithKey((EOEnterpriseObject)e.nextElement(), eo, key);
}
} else {
defaultLog.warn("setValueInObject: Object is neither an EO nor an array");
}
}
}
} else {
if (adaptorType == AdaptorNumberType) {
defaultValue = Integer.valueOf(stringValue);
}
EOGlobalID gid = EOKeyGlobalID.globalIDWithEntityName(relationshipEntityName, new Object[] {defaultValue});
EOEnterpriseObject fault = ec.faultForGlobalID(gid,ec);
eo.addObjectToBothSidesOfRelationshipWithKey(fault,key);
}
}
}
public void readDefaultValues() {
if(_initialDefaultValues == null) {
_initialDefaultValues = new NSMutableDictionary();
EOEntity entity = entity();
NSDictionary entityInfo = (NSDictionary)entity.userInfo().objectForKey("ERXDefaultValues");
for( Enumeration e = entity.attributes().objectEnumerator(); e.hasMoreElements();) {
EOAttribute attr = (EOAttribute)e.nextElement();
String defaultValue = null;
if(attr.userInfo() != null)
defaultValue = (String)attr.userInfo().objectForKey(defaultKey);
if(defaultValue == null && entityInfo != null) {
defaultValue = (String)entityInfo.objectForKey(attr.name());
}
if(defaultValue != null)
setDefaultAttributeValue(attr, defaultValue);
}
for( Enumeration e = entity.relationships().objectEnumerator(); e.hasMoreElements();) {
EORelationship rel = (EORelationship)e.nextElement();
String defaultValue = null;
if(rel.userInfo() != null)
defaultValue = (String)rel.userInfo().objectForKey(defaultKey);
if(defaultValue == null && entityInfo != null) {
defaultValue = (String)entityInfo.objectForKey(rel.name());
}
if(defaultValue != null)
setDefaultRelationshipValue(rel, defaultValue);
}
}
}
public void setDefaultAttributeValue(EOAttribute attr, String defaultValue) {
String name = attr.name();
defaultLog.debug("Adding: {}-{}", name, defaultValue);
AttributeDefault d = new AttributeDefault(attr, defaultValue);
_initialDefaultValues.setObjectForKey(d, name);
}
public void setDefaultRelationshipValue(EORelationship rel, String defaultValue) {
String name = rel.name();
defaultLog.debug("Adding: {}-{}", name, defaultValue);
NSArray attrs = rel.destinationAttributes();
if(!rel.isFlattened() && attrs != null && attrs.count() == 1) {
EOAttribute relAttr = (EOAttribute)attrs.objectAtIndex(0);
if(defaultValue != null) {
RelationshipDefault d = new RelationshipDefault(name, defaultValue, relAttr.adaptorValueType(), rel.destinationEntity().name());
_initialDefaultValues.setObjectForKey(d, name);
}
}
}
public void setDefaultValuesInObject(EOEnterpriseObject eo, EOEditingContext ec) {
defaultLog.debug("About to set values in EO");
if(_initialDefaultValues == null) {
readDefaultValues();
}
for( Enumeration e = _initialDefaultValues.keyEnumerator(); e.hasMoreElements();) {
String key = (String)e.nextElement();
/* A value may have already been set if insertion was done after setting some values.
* For example, _EOSavingProxy.awakeInDistributionContext does this for Java Client apps.
* If so, don't overwrite the existing values.
*/
if (eo.valueForKey(key) == null) {
defaultLog.debug("About to set <{}> in EO", key);
((Default)_initialDefaultValues.objectForKey(key)).setValueInObject(eo);
}
}
}
@Override
public void awakeObjectFromInsertion(EOEnterpriseObject eo, EOEditingContext ec) {
super.awakeObjectFromInsertion(eo, ec);
setDefaultValuesInObject(eo, ec);
}
public String localizedKey(String key) {
key = key + "_" + ERXLocalizer.currentLocalizer().languageCode();
if(!allPropertyKeys().containsObject(key)) {
key = null;
}
return key;
}
@Override
public String inverseForRelationshipKey(String relationshipKey) {
String result = null;
EORelationship relationship = entity().relationshipNamed(relationshipKey);
if(relationship != null && relationship.userInfo() != null) {
result = (String) relationship.userInfo().objectForKey("ERXInverseRelationshipName");
}
if(result == null) {
result = super.inverseForRelationshipKey(relationshipKey);
}
return result;
}
private static boolean useValidity() {
if (useValidity == null) {
useValidity = "true".equals(System.getProperty("er.extensions.ERXGenericRecord.useValidity")) ? Boolean.TRUE : Boolean.FALSE;
}
return useValidity.booleanValue();
}
private void invokeValidityMethodWithType(int type, EOEnterpriseObject eo) throws NSValidation.ValidationException{
try {
Object dummy = null;
Method m = validityMethods()[type];
m.invoke(sharedGSVEngineInstance(), new Object[]{eo});
} catch (IllegalAccessException e1) {
log.error("an exception occured in validityValidateEOObjectOnSave", e1);
} catch (IllegalArgumentException e2) {
log.error("an exception occured in validityValidateEOObjectOnSave", e2);
} catch (NullPointerException e3) {
log.error("an exception occured in validityValidateEOObjectOnSave", e3);
} catch (InvocationTargetException e4) {
Throwable targetException = e4.getTargetException();
if (targetException instanceof NSValidation.ValidationException) {
throw (NSValidation.ValidationException)targetException;
}
log.error("an exception occured in validityValidateEOObjectOnSave", e4);
}
}
private Method[] validityMethods() {
if (validityMethods == null) {
validityMethods = new Method[4];
Method m = methodInSharedGSVEngineInstanceWithName("validateEOObjectOnSave");
validityMethods[0] = m;
m = methodInSharedGSVEngineInstanceWithName("validateEOObjectOnDelete");
validityMethods[1] = m;
m = methodInSharedGSVEngineInstanceWithName("validateEOObjectOnInsert");
validityMethods[2] = m;
m = methodInSharedGSVEngineInstanceWithName("validateEOObjectOnUpdate");
validityMethods[3] = m;
}
return validityMethods;
}
private static Method methodInSharedGSVEngineInstanceWithName(String name) {
try {
return sharedGSVEngineInstance().getClass().getMethod(name, new Class[]{EOEnterpriseObject.class});
} catch (IllegalArgumentException e2) {
throw new NSForwardException(e2);
} catch (NullPointerException e3) {
throw new NSForwardException(e3);
} catch (NoSuchMethodException e4) {
throw new NSForwardException(e4);
}
}
private static Object sharedGSVEngineInstance() {
if (sharedGSVEngineInstance == null) {
try {
Class gsvEngineClass = Class.forName("com.gammastream.validity.GSVEngine");
Method m = gsvEngineClass.getMethod("sharedValidationEngine", new Class[]{});
Object dummy = null;
sharedGSVEngineInstance = m.invoke(dummy, new Object[]{});
} catch (ClassNotFoundException e1) {
throw new NSForwardException(e1);
} catch (NoSuchMethodException e2) {
throw new NSForwardException(e2);
} catch (IllegalAccessException e3) {
throw new NSForwardException(e3);
} catch (InvocationTargetException e4) {
throw new NSForwardException(e4);
}
}
return sharedGSVEngineInstance;
}
@Override
public Class _enforcedKVCNumberClassForKey(String key) {
EOAttribute attribute = entity().attributeNamed(key);
if(attribute != null && attribute.userInfo() != null) {
String className = (String) attribute.userInfo().objectForKey("ERXConstantClassName");
if(className != null) {
Class c = ERXPatcher.classForName(className);
return c;
}
}
return super._enforcedKVCNumberClassForKey(key);
}
private NSMutableArray<Class<ERXPartial>> _partialClasses = new NSMutableArray<Class<ERXPartial>>();
/**
* Associates a partial entity class with this entity.
*
* @see er.extensions.partials
* @param partialClass the partial class to associate
*/
public void _addPartialClass(Class<ERXPartial> partialClass) {
_partialClasses.addObject(partialClass);
}
/**
* Returns the list of partial entity classes for this entity.
*
* @see er.extensions.partials
* @return the list of partial entity classes for this entity
*/
public NSArray<Class<ERXPartial>> partialClasses() {
return _partialClasses;
}
}