PageRenderTime 57ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXEntityClassDescription.java

https://bitbucket.org/molequedeideias/wonder
Java | 1063 lines | 605 code | 87 blank | 371 comment | 131 complexity | a80d3597a9878696bb3fd24466932ff9 MD5 | raw file
  1. /*
  2. * Copyright (C) NetStruxr, Inc. All rights reserved.
  3. *
  4. * This software is published under the terms of the NetStruxr
  5. * Public Software License version 0.5, a copy of which has been
  6. * included with this distribution in the LICENSE.NPL file. */
  7. package er.extensions.eof;
  8. import java.io.File;
  9. import java.lang.reflect.Constructor;
  10. import java.lang.reflect.InvocationTargetException;
  11. import java.lang.reflect.Method;
  12. import java.util.Enumeration;
  13. import org.apache.log4j.Logger;
  14. import com.webobjects.eoaccess.EOAttribute;
  15. import com.webobjects.eoaccess.EOEntity;
  16. import com.webobjects.eoaccess.EOEntityClassDescription;
  17. import com.webobjects.eoaccess.EOModel;
  18. import com.webobjects.eoaccess.EOModelGroup;
  19. import com.webobjects.eoaccess.EORelationship;
  20. import com.webobjects.eocontrol.EOClassDescription;
  21. import com.webobjects.eocontrol.EOEditingContext;
  22. import com.webobjects.eocontrol.EOEnterpriseObject;
  23. import com.webobjects.eocontrol.EOGenericRecord;
  24. import com.webobjects.eocontrol.EOGlobalID;
  25. import com.webobjects.eocontrol.EOKeyGlobalID;
  26. import com.webobjects.eocontrol.EOQualifier;
  27. import com.webobjects.eocontrol.EOQualifierEvaluation;
  28. import com.webobjects.foundation.NSArray;
  29. import com.webobjects.foundation.NSDictionary;
  30. import com.webobjects.foundation.NSForwardException;
  31. import com.webobjects.foundation.NSKeyValueCoding;
  32. import com.webobjects.foundation.NSKeyValueCodingAdditions;
  33. import com.webobjects.foundation.NSMutableArray;
  34. import com.webobjects.foundation.NSMutableDictionary;
  35. import com.webobjects.foundation.NSNotification;
  36. import com.webobjects.foundation.NSNotificationCenter;
  37. import com.webobjects.foundation.NSPropertyListSerialization;
  38. import com.webobjects.foundation.NSSelector;
  39. import com.webobjects.foundation.NSTimestamp;
  40. import com.webobjects.foundation.NSValidation;
  41. import er.extensions.foundation.ERXFileNotificationCenter;
  42. import er.extensions.foundation.ERXFileUtilities;
  43. import er.extensions.foundation.ERXMutableDictionary;
  44. import er.extensions.foundation.ERXPatcher;
  45. import er.extensions.foundation.ERXProperties;
  46. import er.extensions.foundation.ERXStringUtilities;
  47. import er.extensions.foundation.ERXThreadStorage;
  48. import er.extensions.foundation.ERXTimestampUtilities;
  49. import er.extensions.foundation.ERXValueUtilities;
  50. import er.extensions.localization.ERXLocalizer;
  51. import er.extensions.partials.ERXPartial;
  52. import er.extensions.validation.ERXValidationException;
  53. import er.extensions.validation.ERXValidationFactory;
  54. /**
  55. * The main purpose of the ERXClassDescription class is
  56. * to throw {@link ERXValidationException}s instead of the
  57. * usual {@link com.webobjects.foundation.NSValidation.ValidationException
  58. * NSValidation.ValidationException} objects. See the
  59. * ERXValidationException and ERXValidationFactory class
  60. * for more information about localized and templatized
  61. * validation exceptions. This class is configured to
  62. * register itself as the class description by calling
  63. * the method <code>registerDescription</code>. This method
  64. * is called when the principal class of this framework is
  65. * loaded. This happens really early so you shouldn't have
  66. * to worry about this at all.<br/>
  67. * <br />
  68. * Additionally, this class allows for model driven validations in a "poor-mans-Validity-way":
  69. * add a <code>ERXValidation</code> user info entry on your entity.
  70. * This is an example:<code><pre>
  71. * "ERXValidation" = {
  72. * // these keys are evaluated on validateForSave, they don't correspond to properties
  73. * additionalValidationKeys = ("validateEmailPassword");
  74. *
  75. * // This dictionary holds the keys to use for validating properties
  76. * validateForKey =
  77. * {
  78. *
  79. * // these keys are evaluated on validateForSave, they don't correspond to properties
  80. * email =
  81. * (
  82. * {
  83. * // this is the message code into ValidationStrings.plist
  84. * // User.email.wrongLength = "The mail does not have the right size (5 to 50)";
  85. * message = "wrongLength";
  86. *
  87. * // skip this rule if the value is null
  88. * ignoreIfNull = true;
  89. *
  90. * // if there is a qualifier key, then a dictionary containing "object" and "value" is evaluated
  91. * // and an exception is thrown if the evaluation returns false
  92. * qualifier = "(value.length >= 5) AND (value.length < 50)";
  93. * },
  94. * {
  95. * // again, this is the message code into ValidationStrings.plist
  96. * message = "sampleTest";
  97. *
  98. * // Given this key, an object of the corresponding EOQualifierEvaluation subclass is created
  99. * // and given this dictionary on creation. This object needs to be re-entrant.
  100. * className = "SampleTest";
  101. * // an example is:
  102. * // public class SampleTest implements EOQualifierEvaluation {
  103. * // int minLength, maxLength;
  104. * // public SampleTest(Object param) {
  105. * // NSDictionary dict = (NSDictionary)param;
  106. * // minLength = ERXValueUtilities.intValue(dict.objectForKey("minLength"));
  107. * // maxLength = ERXValueUtilities.intValue(dict.objectForKey("maxLength"));
  108. * // }
  109. * // public boolean evaluateObject(Object o) {
  110. * // ERXEntityClassDescription.ValidationObjectValue val
  111. * // = (ERXEntityClassDescription.ValidationObjectValue)o;
  112. * // EOEnterpriseObject eo = val.object();
  113. * // String value = (String)val.value();
  114. * // return value.length() >= minLength && value.length() <= maxLength;
  115. * // }
  116. * // }
  117. *
  118. * minLength = "5";
  119. * maxLength = "10";
  120. * }
  121. * );
  122. *
  123. * // This key does not correspond to any property, it get's evaluated in D2WApps where you have a
  124. * // multi-step page and need to do validation before validateForSave
  125. * "validateEmailPassword" =
  126. * (
  127. * {
  128. * message = "stupidTestWithEmailAndPassword";
  129. *
  130. * // means to get D2W to highlight the fields involved instead of only displaying the message
  131. * // For this to work, your corresponding localized String should be
  132. * // User.email,password.stupidTestWithEmailAndPassword = "Stupid test failed";
  133. * keyPaths = "email,password";
  134. *
  135. * qualifier = "(object.email.length >= object.password.length)";
  136. * }
  137. * );
  138. * };
  139. *
  140. * // These get checked when the object gets saved, additionally to "additionalValidations"
  141. * // The structure of "validateForInsert", "validateForUpdate" and "validateForDelete" is the same.
  142. * validateForSave =
  143. * (
  144. * {
  145. * message = "cantBeBoth";
  146. *
  147. * keyPaths = "isEditor,isAdmin";
  148. *
  149. * qualifier = "(object.isEditor = 'Y' and object.isAdmin = 'Y')";
  150. * }
  151. * );
  152. * }</pre></code>
  153. * This code is mainly a quick-and-dirty rewrite from PRValidation by Proteon.
  154. * <br/>
  155. * Additionally, this class adds a concept of "Default" values that get pushed into the object at creation time.
  156. * 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 attrbute's userInfo<br />
  157. * Example:<pre><code>
  158. * "ERXDefaultValues" = {
  159. *
  160. * // Example for simple values.
  161. * isAdmin = N;
  162. * isEditor = Y;
  163. *
  164. * // Example for a related object (->Languages(pk,displayName)). You need to enter the primary key value.
  165. * language = "de";
  166. *
  167. * // Example for an NSArray of related objects
  168. * recommendingUser = "@threadStorage.actor";
  169. *
  170. * // Example for an NSArray
  171. * articlesToRevisit = "@threadStorage.actor.articles";
  172. *
  173. * // Example for a NSTimestamp. All static methods from ERXTimestampUtilities are supported.
  174. * created = "@now";
  175. * updatePassword = "@tomorrow";
  176. *
  177. * }</pre></code>
  178. * <br/>
  179. * If you wish to provide your own class description subclass
  180. * see the documentation associated with the Factory inner class.
  181. */
  182. public class ERXEntityClassDescription extends EOEntityClassDescription {
  183. /**
  184. * Do I need to update serialVersionUID?
  185. * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
  186. * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
  187. */
  188. private static final long serialVersionUID = 1L;
  189. /** logging support */
  190. public static final Logger log = Logger.getLogger(ERXEntityClassDescription.class);
  191. /** validation logging support */
  192. public static final Logger validationLog = Logger.getLogger("er.validation.ERXEntityClassDescription");
  193. /** default logging support */
  194. public static final Logger defaultLog = Logger.getLogger("er.default.ERXEntityClassDescription");
  195. /** Holds validation info from the entities user info dictionary */
  196. protected NSDictionary _validationInfo;
  197. /** Holds validation qualifiers */
  198. protected NSMutableDictionary _validationQualiferCache;
  199. /** Holds default values */
  200. protected NSMutableDictionary _initialDefaultValues;
  201. /** Holds the default factory instance */
  202. private static Factory _factory;
  203. /** holds validity Methods */
  204. private static Method[] validityMethods = null;
  205. /** index of validity save method */
  206. private static int VALIDITY_SAVE = 0;
  207. /** index of validity delete method */
  208. private static int VALIDITY_DELETE = 1;
  209. /** index of validity insert method */
  210. private static int VALIDITY_INSERT = 2;
  211. /** index of validity update method */
  212. private static int VALIDITY_UPDATE = 3;
  213. /** the shared validity engine instance as Object to eliminate compile errors
  214. * if validity is not linked and should not be used
  215. */
  216. private static Object sharedGSVEngineInstance;
  217. /** Boolean that gets initialized on first use to indicate if validity should
  218. * be used or not, remember that the call System.getProperty acts synchronized
  219. * so this saves some time in multithreaded apps.
  220. */
  221. private static Boolean useValidity;
  222. public static final String ValidateEntityClassAvailability = "ERXEntityClassDescription.validateEntityClassAvailability";
  223. /**
  224. * This factory inner class is registered as the observer
  225. * for three notifications: modelWasAdded, classDescriptionNeededForEntity
  226. * and classDescriptionNeededForClass. If you wish to provide your own
  227. * subclass of ERXEntityClassDescription then you need to create a
  228. * subclass of Factory and set that class name in the system properties
  229. * under the key: <code>er.extensions.ERXClassDescription.factoryClass</code>
  230. * In your Factory subclass override the method: newClassDescriptionForEntity
  231. * to provide your own ERXEntityClassDescription subclass.
  232. */
  233. public static class Factory {
  234. /** Public constructor */
  235. public Factory() {
  236. // Need to be able to preempt the model registering descriptions.
  237. NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("modelWasAdded", ERXConstant.NotificationClassArray), EOModelGroup.ModelAddedNotification, null);
  238. NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("modelGroupWasAdded", ERXConstant.NotificationClassArray), ERXModelGroup.ModelGroupAddedNotification, null);
  239. NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("classDescriptionNeededForEntityName", ERXConstant.NotificationClassArray), EOClassDescription.ClassDescriptionNeededForEntityNameNotification, null);
  240. NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("classDescriptionNeededForClass", ERXConstant.NotificationClassArray), EOClassDescription.ClassDescriptionNeededForClassNotification, null);
  241. }
  242. public void reset() {
  243. _registeredModelNames = new NSMutableArray();
  244. _entitiesForClass = new NSMutableDictionary();
  245. _classDescriptionForEntity = new NSMutableDictionary();
  246. }
  247. protected boolean isRapidTurnaroundEnabled() {
  248. return ERXProperties.booleanForKey("er.extensions.ERXEntityClassDescription.isRapidTurnaroundEnabled");
  249. }
  250. protected boolean isFixingRelationshipsEnabled() {
  251. return ERXProperties.booleanForKey("er.extensions.ERXEntityClassDescription.isFixingRelationshipsEnabled");
  252. }
  253. /**
  254. * Method called when a model group did load.
  255. */
  256. public final void modelGroupWasAdded(NSNotification n) {
  257. log.debug("modelGroupWasAdded: " + n);
  258. EOModelGroup group = (EOModelGroup) n.object();
  259. processModelGroup(group);
  260. }
  261. /**
  262. * Called when a model group finished loading. Checks foreign keys by default. Override to to more...
  263. * @param group
  264. */
  265. protected void processModelGroup(EOModelGroup group) {
  266. for (Enumeration ge = group.models().objectEnumerator(); ge.hasMoreElements();) {
  267. EOModel model = (EOModel)ge.nextElement();
  268. String frameworkName = null;
  269. String modelPath = null;
  270. log.debug("ApplicationDidFinishLaunching: " + model.name());
  271. if(isRapidTurnaroundEnabled()) {
  272. for(Enumeration e = NSArray.componentsSeparatedByString(model.pathURL().getFile(), File.separator).reverseObjectEnumerator(); e.hasMoreElements(); ) {
  273. String a = (String)e.nextElement();
  274. if(a.indexOf(".framework") > 0) {
  275. frameworkName = a.substring(0, a.indexOf(".framework"));
  276. break;
  277. }
  278. }
  279. if(frameworkName == null) {
  280. frameworkName = "app";
  281. }
  282. modelPath = ERXFileUtilities.pathForResourceNamed(model.name() + ".eomodeld", frameworkName, null);
  283. defaultLog.debug("Path for model <" + model.name() + "> in framework <"+frameworkName+">: " + modelPath);
  284. }
  285. for (Enumeration ee = model.entities().objectEnumerator(); ee.hasMoreElements();) {
  286. EOEntity entity = (EOEntity)ee.nextElement();
  287. checkForeignKeys(entity);
  288. EOClassDescription cd = EOClassDescription.classDescriptionForEntityName(entity.name());
  289. defaultLog.debug("Reading defaults for: " + entity.name());
  290. if(cd instanceof ERXEntityClassDescription) {
  291. ((ERXEntityClassDescription)cd).readDefaultValues();
  292. if(isRapidTurnaroundEnabled() && modelPath != null) {
  293. String path = modelPath + File.separator + entity.name() + ".plist";
  294. ERXFileNotificationCenter.defaultCenter().addObserver(cd, new NSSelector("modelFileDidChange", ERXConstant.NotificationClassArray), path);
  295. }
  296. } else {
  297. defaultLog.warn("Entity classDescription is not ERXEntityClassDescription: " + entity.name());
  298. }
  299. }
  300. }
  301. }
  302. /**
  303. * Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
  304. * when an EOModel is loaded.
  305. * This method just calls the method
  306. * <code>registerDescriptionForEntitiesInModel</code>
  307. *
  308. * @param n notification that has the EOModel that was loaded.
  309. */
  310. public final void modelWasAdded(NSNotification n) {
  311. EOModel model = ((EOModel)n.object());
  312. log.debug("ModelWasAddedNotification: " + model.name());
  313. // Don't want this guy getting in our way.
  314. NSNotificationCenter.defaultCenter().removeObserver(model);
  315. try {
  316. registerDescriptionForEntitiesInModel(model);
  317. } catch (RuntimeException e) {
  318. log.error("Error registering model: " + model.name(), e);
  319. throw e;
  320. }
  321. }
  322. /**
  323. * Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
  324. * when a class description is needed
  325. * for a given entity. Usually this method isn't needed seeing
  326. * as we preempt the on demand loading of class descriptions
  327. * by loading all of them when the EOModel is loaded.
  328. * This method just calls the method
  329. * <code>registerDescriptionForEntity</code>
  330. *
  331. * @param n notification that has the name of the entity
  332. * that needs the class description.
  333. */
  334. public void classDescriptionNeededForEntityName(NSNotification n) {
  335. log.debug("classDescriptionNeededForEntityName: " + (String)n.object());
  336. String name = (String)n.object();
  337. EOEntity e = ERXEOAccessUtilities.entityNamed(null,name);
  338. if(e == null) log.error("Entity " + name + " not found in the default model group!");
  339. if (e != null) {
  340. registerDescriptionForEntity(e);
  341. }
  342. }
  343. /**
  344. * Method called by the {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}
  345. * when a class description is needed
  346. * for a given Class. Usually this method isn't needed seeing
  347. * as we preempt the on demand loading of class descriptions
  348. * by loading all of them when the EOModel is loaded.
  349. * This method just calls the method
  350. * <code>registerDescriptionForClass</code>
  351. * @param n notification that has the Class object
  352. * that needs a class description.
  353. */
  354. public void classDescriptionNeededForClass(NSNotification n) {
  355. Class c = (Class)n.object();
  356. log.debug("classDescriptionNeededForClass: " + c.getName());
  357. registerDescriptionForClass(c);
  358. }
  359. /**
  360. * Factory method that is used to create a new class
  361. * description for a given entity. Sub classes that
  362. * wish to provide a sub class of ERXEntityClassDescription
  363. * should override this method to create that custom
  364. * description. By default this method returns a new
  365. * ERXEntityClassDescription.
  366. * @param entity to create the class description for
  367. * @return new class description for the given entity
  368. */
  369. protected ERXEntityClassDescription newClassDescriptionForEntity(EOEntity entity) {
  370. String key = entity.name();
  371. EOModel model = entity.model();
  372. if (model != null) {
  373. key = model.name() + " " + key;
  374. }
  375. ERXEntityClassDescription classDescription = (ERXEntityClassDescription)_classDescriptionForEntity.objectForKey(key);
  376. if (classDescription == null) {
  377. classDescription = new ERXEntityClassDescription(entity);
  378. _classDescriptionForEntity.setObjectForKey(classDescription, key);
  379. }
  380. return classDescription;
  381. }
  382. /** holds a reference to all of the registered model names */
  383. private NSMutableArray _registeredModelNames = new NSMutableArray();
  384. /** holds a mapping of class to entities */
  385. private NSMutableDictionary _entitiesForClass = new NSMutableDictionary();
  386. /** holds a mapping of entity to class descriptions */
  387. private NSMutableDictionary _classDescriptionForEntity = new NSMutableDictionary();
  388. /**
  389. * Allows for entities to be altered
  390. * before they have a custom class description
  391. * registered. Sub classes can override this method
  392. * to provide any extra alterings before the description
  393. * is registered. However be sure to call super as this
  394. * method does convert the class name from EOGenericRecord
  395. * to ERXGenericRecord, which unfortunately is required
  396. * for custom validation to work at the moment.
  397. * @param eoentity to be prepared for registration
  398. */
  399. protected void prepareEntityForRegistration(EOEntity eoentity) {
  400. String className = eoentity.className();
  401. String defaultClassName = ERXProperties.stringForKeyWithDefault("er.extensions.ERXEntityClassDescription.defaultClassName", ERXGenericRecord.class.getName());
  402. String alternateClassName = ERXProperties.stringForKey("er.extensions.ERXEntityClassDescription." + eoentity.name() + ".ClassName");
  403. if (alternateClassName != null) {
  404. log.debug(eoentity.name() + ": setting class from: " + className + " to: " + alternateClassName);
  405. eoentity.setClassName(alternateClassName);
  406. } else if (className.equals("EOGenericRecord")) {
  407. eoentity.setClassName(defaultClassName);
  408. }
  409. }
  410. /**
  411. * Handles errors when an optional relationship has a source attribute
  412. * that is set to allow null values. Subclasses can override this to do more specific handling.
  413. */
  414. protected void handleOptionalRelationshipError(EOEntity eoentity, EORelationship relationship, EOAttribute attribute) {
  415. if(isFixingRelationshipsEnabled()) {
  416. relationship.setIsMandatory(true);
  417. log.info(eoentity.name() + ": relationship '"
  418. + relationship.name() + "' was switched to mandatory, because the foreign key '"
  419. + attribute.name() + "' does NOT allow NULL values");
  420. } else {
  421. log.warn(eoentity.name() + ": relationship '"
  422. + relationship.name() + "' is marked to-one and optional, but the foreign key '"
  423. + attribute.name() + "' does NOT allow NULL values");
  424. }
  425. }
  426. /**
  427. * Handles errors when a mandatory relationship has a source attribute
  428. * that is set to not allow null values. Subclasses can override this to do more specific handling.
  429. */
  430. protected void handleMandatoryRelationshipError(EOEntity eoentity, EORelationship relationship, EOAttribute attribute) {
  431. if(isFixingRelationshipsEnabled()) {
  432. relationship.setIsMandatory(false);
  433. log.info(eoentity.name() + ": relationship '"
  434. + relationship.name() + "' was switched to optional, because the foreign key '"
  435. + attribute.name() + "' allows NULL values");
  436. } else {
  437. log.warn(eoentity.name() + ": relationship '"
  438. + relationship.name() + "' is marked to-one and mandatory, but the foreign key '"
  439. + attribute.name() + "' allows NULL values");
  440. }
  441. }
  442. /**
  443. * Checks for foreign keys that are <code>NOT NULL</code>,
  444. * but whose relationship is marked as non-mandatory and vice-versa. This
  445. * error is not checked by EOModeler, so we do it here.
  446. * @param eoentity to be check
  447. */
  448. public void checkForeignKeys(EOEntity eoentity) {
  449. NSArray primaryKeys = eoentity.primaryKeyAttributes();
  450. for(Enumeration relationships = eoentity.relationships().objectEnumerator(); relationships.hasMoreElements(); ) {
  451. EORelationship relationship = (EORelationship)relationships.nextElement();
  452. if(!relationship.isToMany()) {
  453. if(relationship.isMandatory()) {
  454. for(Enumeration attributes = relationship.sourceAttributes().objectEnumerator(); attributes.hasMoreElements(); ) {
  455. EOAttribute attribute = (EOAttribute)attributes.nextElement();
  456. if(attribute.allowsNull()) {
  457. handleMandatoryRelationshipError(eoentity, relationship, attribute);
  458. }
  459. }
  460. } else {
  461. for(Enumeration attributes = relationship.sourceAttributes().objectEnumerator(); attributes.hasMoreElements(); ) {
  462. EOAttribute attribute = (EOAttribute)attributes.nextElement();
  463. if(!attribute.allowsNull() && !primaryKeys.containsObject(attribute)) {
  464. handleOptionalRelationshipError(eoentity, relationship, attribute);
  465. }
  466. }
  467. }
  468. }
  469. }
  470. }
  471. /**
  472. * This method registers custom class descriptions for all
  473. * of the entities in a given model. This method is called
  474. * when a model is loaded. The reason for this method is
  475. * to preempt the usual class description loading mechanism
  476. * which has a race condition involved for the order in
  477. * which the notifications are recieved.
  478. * @param model that contains all of the entities to be registerd
  479. */
  480. protected void registerDescriptionForEntitiesInModel(EOModel model) {
  481. if (!_registeredModelNames.containsObject(model.name())) {
  482. for (Enumeration e = model.entities().objectEnumerator(); e.hasMoreElements();) {
  483. EOEntity eoentity = (EOEntity)e.nextElement();
  484. String className = eoentity.className();
  485. prepareEntityForRegistration(eoentity);
  486. NSMutableArray array = (NSMutableArray)_entitiesForClass.objectForKey(className);
  487. if(array == null) {
  488. array = new NSMutableArray();
  489. }
  490. if (log.isDebugEnabled())
  491. log.debug("Adding entity " +eoentity.name()+ " with class " + eoentity.className());
  492. array.addObject(eoentity);
  493. _entitiesForClass.setObjectForKey(array, eoentity.className());
  494. //HACK ALERT: (ak) We work around classDescriptionForNewInstances() of EOEntity being broken here...
  495. registerDescriptionForEntity(eoentity);
  496. }
  497. _registeredModelNames.addObject(model.name());
  498. }
  499. // Don't want this guy getting in our way later on ;
  500. NSNotificationCenter.defaultCenter().removeObserver(model);
  501. }
  502. /**
  503. * This is a hack to work around RadarBug:2867501. EOEntity
  504. * is hardwired to return an EOEntityClassdescription for the
  505. * method classDescriptionForNewInstances, this causes a serious
  506. * problem when using custom class descriptions with D2W which
  507. * makes use of this method. What this hack does is use the magic
  508. * of key-value coding to push our custom class description onto
  509. * a given entity. In order to do this we needed to add the
  510. * custom {@link KVCProtectedAccessor} to the package
  511. * com.webobjects.eoaccess.
  512. * @param entity to have the custom class description set on
  513. * @param cd class description to set on the entity
  514. */
  515. private void _setClassDescriptionOnEntity(EOEntity entity, ERXEntityClassDescription cd) {
  516. try {
  517. //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
  518. NSKeyValueCoding.Utility.takeValueForKey(entity, cd, "classDescription");
  519. } catch(RuntimeException ex) {
  520. log.warn("_setClassDescriptionOnEntity: " + ex);
  521. }
  522. }
  523. /**
  524. * Registers a custom class description for the given
  525. * entity using the method <code>newClassDescriptionForEntity</code>
  526. * which can be overridden by subclasses to provide a
  527. * different class description subclass.
  528. * @param entity to register the class description for
  529. */
  530. protected void registerDescriptionForEntity(EOEntity entity) {
  531. Class entityClass = EOGenericRecord.class;
  532. String className = entity.className();
  533. if (log.isDebugEnabled()) {
  534. log.debug("Registering description for entity: " + entity.name() + " with class: " + className);
  535. }
  536. if (ERXProperties.booleanForKeyWithDefault(ValidateEntityClassAvailability, true)) { // Make it possible to opt-out of this check.
  537. try {
  538. entityClass = className.endsWith("EOGenericRecord") ? EOGenericRecord.class : Class.forName(className);
  539. } catch (java.lang.ClassNotFoundException ex) {
  540. 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);
  541. }
  542. }
  543. ERXEntityClassDescription cd = newClassDescriptionForEntity(entity);
  544. EOClassDescription.registerClassDescription(cd, entityClass);
  545. _setClassDescriptionOnEntity(entity, cd);
  546. }
  547. /**
  548. * This method is called when a class description is
  549. * needed for a particular class. Here we use the
  550. * previous cache that we constructed of class to
  551. * entity map when the models were loaded. In this
  552. * way we can register all of the custom class
  553. * descriptions for a given class if need be.
  554. * @param class1 class object to have a custom class
  555. * description registered for.
  556. */
  557. protected void registerDescriptionForClass(Class class1) {
  558. NSArray entities = (NSArray)_entitiesForClass.objectForKey(class1.getName());
  559. if (entities != null) {
  560. if (log.isDebugEnabled())
  561. log.debug("Registering descriptions for class: " + class1.getName() + " found entities: " + entities.valueForKey("name"));
  562. for (Enumeration e = entities.objectEnumerator(); e.hasMoreElements();) {
  563. EOEntity entity = (EOEntity)e.nextElement();
  564. ERXEntityClassDescription cd = newClassDescriptionForEntity(entity);
  565. EOClassDescription.registerClassDescription(cd, class1);
  566. _setClassDescriptionOnEntity(entity, cd);
  567. }
  568. } else {
  569. if(class1.getName().indexOf('$') < 0) {
  570. log.error("Unable to register descriptions for class: " + class1.getName(), new RuntimeException("Dummy"));
  571. }
  572. }
  573. }
  574. }
  575. /** getter for the factory */
  576. public static Factory factory() {
  577. return _factory;
  578. }
  579. /**
  580. * This method is called by the principal class
  581. * of the framework when the framework's NSBundle is
  582. * loaded. This method registers an observer, either
  583. * a Factory object, which is an inner class of this class
  584. * or a custom Factory subclass specified in the property:
  585. * <b>er.extensions.ERXClassDescription.factoryClass</b>.
  586. * This observer listens for notifications when a model
  587. * is loaded or a class description is needed and responds
  588. * by creating and registering custom class descriptions.
  589. */
  590. public static void registerDescription() {
  591. if (_factory == null) {
  592. _factory = null;
  593. try {
  594. String className = ERXProperties.stringForKey("er.extensions.ERXClassDescription.factoryClass");
  595. if (className != null) {
  596. _factory = (Factory)Class.forName(className).newInstance();
  597. }
  598. } catch(Exception ex) {
  599. log.warn("Exception while registering factory, using default: " + ex );
  600. }
  601. if(_factory == null)
  602. _factory=new Factory();
  603. }
  604. }
  605. /**
  606. * Public constructor
  607. * @param entity that this class description corresponds to
  608. */
  609. public ERXEntityClassDescription(EOEntity entity) {
  610. super(entity);
  611. _validationInfo = ERXValueUtilities.dictionaryValue(entity.userInfo().objectForKey("ERXValidation"));
  612. _validationQualiferCache = ERXMutableDictionary.synchronizedDictionary();
  613. }
  614. public void modelFileDidChange(NSNotification n) {
  615. File file = (File)n.object();
  616. try {
  617. defaultLog.debug("Reading .plist for entity <"+entity()+">");
  618. NSDictionary userInfo = (NSDictionary)NSPropertyListSerialization.propertyListFromString(ERXFileUtilities.stringFromFile(file));
  619. entity().setUserInfo((NSDictionary)userInfo.objectForKey("userInfo"));
  620. _validationInfo = ERXValueUtilities.dictionaryValue(entity().userInfo().objectForKey("ERXValidation"));
  621. _validationQualiferCache = ERXMutableDictionary.synchronizedDictionary();
  622. _initialDefaultValues = null;
  623. readDefaultValues();
  624. } catch(Exception ex) {
  625. defaultLog.error("Can't read file <"+file.getAbsolutePath()+">", ex);
  626. }
  627. }
  628. /**
  629. * This method is called when an object is
  630. * about to be deleted. If any validation
  631. * exceptions occur they are converted to an
  632. * {@link ERXValidationException} and that is
  633. * thrown.
  634. * @param obj enterprise object to be deleted
  635. * @throws NSValidation.ValidationException validation exception
  636. */
  637. @Override
  638. public void validateObjectForDelete(EOEnterpriseObject obj) throws NSValidation.ValidationException {
  639. try {
  640. if (useValidity()) {
  641. invokeValidityMethodWithType(VALIDITY_DELETE, obj);
  642. }
  643. super.validateObjectForDelete(obj);
  644. validateObjectWithUserInfo(obj, null, "validateForDelete", "validateForDelete");
  645. } catch (ERXValidationException eov) {
  646. throw eov;
  647. } catch (NSValidation.ValidationException eov) {
  648. if (log.isDebugEnabled())
  649. log.debug("Caught validation exception: " + eov);
  650. ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
  651. throw (erv != null ? erv : eov);
  652. }
  653. }
  654. /**
  655. * Overridden to perform a check if the entity is still in a model group.
  656. * This can happen if you remove the entity, clone it to change things and re-add it afterwards.
  657. */
  658. @Override
  659. public EOEntity entity() {
  660. checkEntity();
  661. return super.entity();
  662. }
  663. protected void checkEntity() {
  664. if(_entity.model() == null) {
  665. try {
  666. EOEntity registeredEntity = ERXEOAccessUtilities.entityNamed(null,_entity.name());
  667. if(registeredEntity != null) {
  668. _entity = registeredEntity;
  669. } else {
  670. EOModel model = _entity.model();
  671. if(model == null) {
  672. model = ERXEOAccessUtilities.modelGroup(null).models().lastObject();
  673. }
  674. model.addEntity(_entity);
  675. log.warn("Added <" + _entity.name() + "> to default model group.");
  676. }
  677. } catch (Exception ex) {
  678. throw new RuntimeException("Model or modelgroup for <" + _entity.name() + "> is null: " + entity().model(), ex);
  679. }
  680. }
  681. }
  682. @Override
  683. public EOEnterpriseObject createInstanceWithEditingContext(EOEditingContext ec, EOGlobalID gid) {
  684. checkEntity();
  685. return super.createInstanceWithEditingContext(ec, gid);
  686. }
  687. /**
  688. * This method is called when an object is
  689. * about to be updated. If any validation
  690. * exceptions occur they are converted to an
  691. * {@link ERXValidationException} and that is
  692. * thrown.
  693. * @param obj enterprise object to be deleted
  694. * @throws NSValidation.ValidationException validation exception
  695. */
  696. public void validateObjectForUpdate(EOEnterpriseObject obj) throws NSValidation.ValidationException {
  697. try {
  698. if (useValidity()) {
  699. invokeValidityMethodWithType(VALIDITY_UPDATE, obj);
  700. }
  701. validateObjectWithUserInfo(obj, null, "validateForUpdate", "validateForUpdate");
  702. } catch (ERXValidationException eov) {
  703. throw eov;
  704. } catch (NSValidation.ValidationException eov) {
  705. if (log.isDebugEnabled())
  706. log.debug("Caught validation exception: " + eov);
  707. ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
  708. throw (erv != null ? erv : eov);
  709. }
  710. }
  711. /**
  712. * This method is called when an object is
  713. * about to be inserted. If any validation
  714. * exceptions occur they are converted to an
  715. * {@link ERXValidationException} and that is
  716. * thrown.
  717. * @param obj enterprise object to be deleted
  718. * @throws NSValidation.ValidationException validation exception
  719. */
  720. public void validateObjectForInsert(EOEnterpriseObject obj) throws NSValidation.ValidationException {
  721. try {
  722. if (useValidity()) {
  723. invokeValidityMethodWithType(VALIDITY_INSERT, obj);
  724. }
  725. validateObjectWithUserInfo(obj, null, "validateForInsert", "validateForInsert");
  726. } catch (ERXValidationException eov) {
  727. throw eov;
  728. } catch (NSValidation.ValidationException eov) {
  729. if (log.isDebugEnabled())
  730. log.debug("Caught validation exception: " + eov);
  731. ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
  732. throw (erv != null ? erv : eov);
  733. }
  734. }
  735. /**
  736. * This method is called to validate a value
  737. * for a particular key. Typical validation
  738. * exceptions that might occur are non-null
  739. * constraints or string is greater in length
  740. * than is allowed. If a validation
  741. * exception does occur they are converted to an
  742. * {@link ERXValidationException} and that is
  743. * thrown.
  744. * @param obj value to be validated
  745. * @param s property key to validate the value
  746. * against.
  747. * @throws NSValidation.ValidationException validation exception
  748. */
  749. @Override
  750. public Object validateValueForKey(Object obj, String s) throws NSValidation.ValidationException {
  751. Object validated = null;
  752. if (log.isDebugEnabled())
  753. log.debug("Validate value: " + obj + " for key: " + s);
  754. try {
  755. if(obj instanceof ERXConstant) {
  756. validated = obj;
  757. } else {
  758. validated = super.validateValueForKey(obj, s);
  759. }
  760. } catch (ERXValidationException eov) {
  761. throw eov;
  762. } catch (NSValidation.ValidationException eov) {
  763. if (log.isDebugEnabled())
  764. log.debug("Caught validation exception: " + eov);
  765. ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
  766. throw (erv != null ? erv : eov);
  767. }
  768. return validated;
  769. }
  770. /**
  771. * This method is called when an object is
  772. * about to be saved. Adds support for extra validation keys to
  773. * be set in an array in the entity's userInfo under the keypath
  774. * <code>ERXValidation.additionalValidationKeys</code>. If any validation
  775. * exceptions occur they are converted to an
  776. * {@link ERXValidationException} and that is
  777. * thrown.
  778. * @param obj enterprise object to be saved
  779. * @throws NSValidation.ValidationException validation exception
  780. */
  781. @Override
  782. public void validateObjectForSave(EOEnterpriseObject obj) throws NSValidation.ValidationException {
  783. try {
  784. if (useValidity()) {
  785. invokeValidityMethodWithType(VALIDITY_SAVE, obj);
  786. }
  787. if(_validationInfo != null) {
  788. NSArray additionalValidationKeys = (NSArray)_validationInfo.objectForKey("additionalValidationKeys");
  789. if(additionalValidationKeys != null) {
  790. for(Enumeration e = additionalValidationKeys.objectEnumerator(); e.hasMoreElements();) {
  791. String key = (String)e.nextElement();
  792. NSSelector selector = new NSSelector(key);
  793. if(selector.implementedByObject(obj)) {
  794. try {
  795. selector.invoke(obj);
  796. } catch (Exception ex) {
  797. if(ex instanceof NSValidation.ValidationException)
  798. throw (NSValidation.ValidationException)ex;
  799. log.error(ex);
  800. }
  801. } else {
  802. validateObjectWithUserInfo(obj, null, "validateForKey." + key, key);
  803. }
  804. }
  805. }
  806. }
  807. validateObjectWithUserInfo(obj, null, "validateForSave", "validateForSave");
  808. } catch (ERXValidationException eov) {
  809. throw eov;
  810. } catch (NSValidation.ValidationException eov) {
  811. if (log.isDebugEnabled())
  812. log.debug("Caught validation exception: " + eov);
  813. ERXValidationException erv = ERXValidationFactory.defaultFactory().convertException(eov, obj);
  814. throw (erv != null ? erv : eov);
  815. }
  816. }
  817. public static class ValidationObjectValue {
  818. protected EOEnterpriseObject object;
  819. protected Object value;
  820. public ValidationObjectValue(EOEnterpriseObject object, Object value) {
  821. this.object = object;
  822. this.value = value;
  823. }
  824. public Object value() { return value;}
  825. public EOEnterpriseObject object() { return object;}
  826. }
  827. public static class QualiferValidation implements EOQualifierEvaluation {
  828. protected EOQualifier qualifier;
  829. public QualiferValidation(Object info) {
  830. NSDictionary dict =(NSDictionary)info;
  831. qualifier = EOQualifier.qualifierWithQualifierFormat((String)dict.objectForKey("qualifier"), null);
  832. }
  833. public boolean evaluateWithObject(Object o) {
  834. return qualifier.evaluateWithObject(o);
  835. }
  836. }
  837. protected boolean validateObjectValueDictWithInfo(ValidationObjectValue values, NSDictionary info, String cacheKey) {
  838. EOQualifierEvaluation q = (EOQualifierEvaluation)_validationQualiferCache.objectForKey(cacheKey);
  839. if(q == null) {
  840. try {
  841. String className = (String)info.objectForKey("className");
  842. if(className == null) {
  843. className = QualiferValidation.class.getName();
  844. }
  845. Class cl = ERXPatcher.classForName(className);
  846. Constructor co = cl.getConstructor(new Class [] {Object.class});
  847. Object o = co.newInstance(new Object[] {info});
  848. q = (EOQualifierEvaluation)o;
  849. } catch(Exception ex) {
  850. throw new NSForwardException(ex);
  851. }
  852. _validationQualiferCache.setObjectForKey(q, cacheKey);
  853. }
  854. if(values.value() == null && "true".equals(info.objectForKey("ignoreIfNull")))
  855. return true;
  856. if(q != null)
  857. return q.evaluateWithObject(values);
  858. return true;
  859. }
  860. public void validateObjectWithUserInfo(EOEnterpriseObject object, Object value, String validationTypeString, String property) {
  861. if(_validationInfo != null) {
  862. NSArray qualifiers = (NSArray)_validationInfo.valueForKeyPath(validationTypeString);
  863. if(qualifiers != null) {
  864. ValidationObjectValue values = new ValidationObjectValue(object, value);
  865. int i = 0;
  866. for(Enumeration e = qualifiers.objectEnumerator(); e.hasMoreElements();) {
  867. NSDictionary info = (NSDictionary)e.nextElement();
  868. if(validationLog.isDebugEnabled())
  869. validationLog.debug("Validate " + validationTypeString +"."+ property +" with <"+ value + "> on " + object + "\nRule: " + info);
  870. if(!validateObjectValueDictWithInfo(values, info, validationTypeString+property+i)) {
  871. String message = (String)info.objectForKey("message");
  872. String keyPaths = (String)info.objectForKey("keyPaths");
  873. property = keyPaths == null ? property : keyPaths;
  874. if(validationLog.isDebugEnabled())
  875. validationLog.info("Validation failed " + validationTypeString +"."+ property +" with <"+ value + "> on " + object);
  876. throw ERXValidationFactory.defaultFactory().createException(object, property, value,message);
  877. }
  878. i = i+1;
  879. }
  880. }
  881. }
  882. }
  883. /**
  884. * Calculates a display name for a key using
  885. * localization of entityname.key if found
  886. * otherwise an improved method.
  887. * @param key to be converted
  888. * @return pretty display name
  889. */
  890. @Override
  891. public String displayNameForKey(String key) {
  892. if (ERXLocalizer.isLocalizationEnabled()) {
  893. return ERXLocalizer.currentLocalizer().localizedDisplayNameForKey(this, key);
  894. }
  895. return ERXStringUtilities.displayNameForKey(key);
  896. }
  897. /**** default handling */
  898. // Default handling from here on
  899. protected String defaultKey = "default";
  900. public static interface Default {
  901. public static final int AdaptorNumberType = 0;
  902. public static final int AdaptorCharactersType = 1;
  903. public static final int AdaptorBytesType = 2;
  904. public static final int AdaptorDateType = 3;
  905. public void setValueInObject(EOEnterpriseObject eo);
  906. }
  907. public static class AttributeDefault implements Default {
  908. String key;
  909. String stringValue;
  910. int adaptorType;
  911. EOAttribute attribute;
  912. public AttributeDefault(EOAttribute attribute, String stringValue) {
  913. this(attribute.name(), stringValue, attribute.adaptorValueType());
  914. this.attribute = attribute;
  915. }
  916. public AttributeDefault(String key, String stringValue, int adaptorType) {
  917. this.key = key;
  918. this.stringValue = stringValue;
  919. this.adaptorType = adaptorType;
  920. }
  921. public AttributeDefault(String key, String stringValue) {
  922. this(key, stringValue, AdaptorCharactersType);
  923. }
  924. public void setValueInObject(EOEnterpriseObject eo) {
  925. Object defaultValue = stringValue;
  926. if(stringValue.startsWith("@threadStorage.")) {
  927. String keyPath = stringValue.substring("@threadStorage.".length());
  928. defaultValue = ERXThreadStorage.valueForKeyPath(keyPath);
  929. } else {
  930. if(attribute != null && attribute.valueFactoryMethodName() != null && attribute.factoryMethodArgumentType() == EOAttribute.FactoryMethodArgumentIsString) {
  931. defaultValue = attribute.newValueForString(stringValue);
  932. }
  933. }
  934. if(defaultValue != null) {
  935. String s = defaultValue.toString();
  936. s = s.substring(s.indexOf("@")+1);
  937. if(adaptorType == AdaptorDateType) {
  938. defaultValue = ERXTimestampUtilities.timestampForString(s);
  939. } else if (adaptorType == AdaptorNumberType) {
  940. NSTimestamp temp = ERXTimestampUtilities.timestampForString(s);
  941. if(temp != null) {
  942. defaultValue = ERXTimestampUtilities.unixTimestamp(temp);
  943. } else {
  944. //the value will be coerced by the eo...
  945. defaultValue = ERXValueUtilities.bigDecimalValue(s);
  946. }
  947. }
  948. }
  949. eo.takeValueForKey(defaultValue, key);
  950. }
  951. }
  952. public static class RelationshipDefault implements Default {
  953. String key;
  954. String stringValue;
  955. int adaptorType;
  956. String relationshipEntityName;
  957. public RelationshipDefault(String key, String stringValue, int adaptorType, String relationshipEntityName) {
  958. this.key = key;
  959. this.stringValue = stringValue;
  960. this.adaptorType = adaptorType;
  961. this.relationshipEntityName = relationshipEntityName;
  962. }
  963. public void setValueInObject(EOEnterpriseObject eo) {
  964. Object defaultValue = stringValue;
  965. EOEditingContext ec = eo.editingContext();
  966. if(stringValue.charAt(0) == '@') { // computed key
  967. if(stringValue.equals("@new")) {
  968. EOClassDescription cd = EOClassDescription.classDescriptionForEntityName(relationshipEntityName);
  969. EOEnterpriseObject newObject = cd.createInstanceWithEditingContext(eo.editingContext(), null);
  970. ec.insertObject(newObject);
  971. eo.addObjectToBothSidesOfRelationshipWithKey(newObject,key);
  972. } else if(stringValue.startsWith("@threadStorage.")) {
  973. String keyPath = stringValue.substring("@threadStorage.".length());
  974. Object o = ERXThreadStorage.valueForKey(keyPath);
  975. if(keyPath.indexOf(".") > 0) {
  976. keyPath = stringValue.substrin