summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOClassDescription.java
blob: 79019de795d61fe17bef7c79d5b04e846e3aaa91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
/*
Wotonomy: OpenStep design patterns for pure Java applications.
Copyright (C) 2001 Intersect Software Corporation

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see http://www.gnu.org
*/

package net.wotonomy.control;

import java.util.HashMap;
import java.util.Map;

import net.wotonomy.foundation.NSArray;
import net.wotonomy.foundation.NSMutableArray;
import net.wotonomy.foundation.NSNotificationCenter;
import net.wotonomy.foundation.internal.Introspector;
import net.wotonomy.foundation.internal.WotonomyException;

/**
 * EOClassDescription provides meta-information about a class and is used to
 * customize certain behaviors within wotonomy and specifically within editing
 * contexts and object stores. <br>
 * <br>
 *
 * The default implementation works for most well-formed java beans, but you
 * will want to create your own subclass most typically to customize the toOne
 * and toMany relationships for your class to ensure that an entire graph of
 * objects is not persisted in order to perist a single object. <br>
 * <br>
 *
 * The easiest way to register your subclass is to create it in the same package
 * as the class it describes but with a "ClassDesc" suffix. For example,
 * "my.package.MyEntity" would be described by "my.package.MyEntityClassDesc".
 * <br>
 * <br>
 * 
 * Note that while the interface is the same, the implementation of this class
 * differs substantially from the specification in order to be more useful for
 * java classes.
 *
 * @author michael@mpowers.net
 * @author $Author: cgruber $
 * @version $Revision: 900 $
 */
public class EOClassDescription {
	/**
	 * A delete rule specifying that object(s) that reference this object should
	 * have those references set to null when this object is deleted.
	 */
	public static final int DeleteRuleNullify = 0;

	/**
	 * A delete rule specifying that object(s) referenced by this object should also
	 * be deleted when this object is deleted.
	 */
	public static final int DeleteRuleCascade = 1;

	/**
	 * A delete rule specicying that this object should not be allowed to be deleted
	 * if it references any object(s).
	 */
	public static final int DeleteRuleDeny = 2;

	/**
	 * A delete rule specifying that no action be taken when this object is deleted.
	 * This is the default.
	 */
	public static final int DeleteRuleNoAction = 3;

	/**
	 * Notification fired when a class description has been requested for a class.
	 * Observers should watch for this notification and call
	 * registerClassDescription so that class descriptions can be loaded on-demand.
	 * The notification's object is the requested class and the user info dictionary
	 * is null.
	 */
	public static final String ClassDescriptionNeededForClassNotification = "ClassDescriptionNeededForClassNotification";

	/**
	 * Notification fired when a class description has been requested for an entity
	 * name. Observers should watch for this notification and call
	 * registerClassDescription so that class descriptions can be loaded on-demand.
	 * The notification's object is the requested name and the user info dictionary
	 * is null.
	 */
	public static final String ClassDescriptionNeededForEntityNameNotification = "ClassDescriptionNeededForEntityNameNotification";

	public EOClassDescription() {
		super();
	}

	/**
	 * Returns the class description that corresponds to the specified class. If the
	 * class description has not already been loaded, a
	 * ClassDescriptionNeededForClassNotification is posted. If the class
	 * description is still not found, the class' loader is consulted for a class in
	 * the same package and named the same as the specified class but appended with
	 * "ClassDesc", e.g. "EmployeeObjectClassDesc". If the class description is
	 * still not found, a class description is returned that uses java bean
	 * introspection to provide reasonable values.
	 */
	public static EOClassDescription classDescriptionForClass(Class aClass) {
		if (classMap == null)
			classMap = new HashMap();
		EOClassDescription result = (EOClassDescription) classMap.get(aClass);
		if (result == null) {
			// if not found, post notification
			NSNotificationCenter.defaultCenter().postNotification(ClassDescriptionNeededForClassNotification, aClass,
					null);
			result = (EOClassDescription) classMap.get(aClass);
		}
		if (result == null) {
			// if not found, look for similarly named class
			String className = aClass.getName() + ClassNameSuffix;
			Class classDesc;
			try {
				classDesc = aClass.getClassLoader().loadClass(className);
				if (classDesc != null) {
					result = (EOClassDescription) classDesc.newInstance();
					registerClassDescription(result, aClass);
				}
			} catch (Exception exc) {
				// ignore exceptions and resume
			}
		}
		if (result == null) {
			// if not found, default to this class
			result = new EOClassDescription(aClass);
			registerClassDescription(result, aClass);
		}
		return result;
	}

	/**
	 * Returns the class description that corresponds to the specified entity name.
	 * If the class description has not already been loaded, a
	 * ClassDescriptionNeededForEntityNameNotification is posted. Returns null if no
	 * class description can be found for the entity name.
	 */
	public static EOClassDescription classDescriptionForEntityName(String aName) {
		if (entityMap == null)
			entityMap = new HashMap();
		EOClassDescription result = (EOClassDescription) entityMap.get(aName);
		if (result == null) {
			// if not found, post notification
			NSNotificationCenter.defaultCenter().postNotification(ClassDescriptionNeededForEntityNameNotification,
					aName, null);
			result = (EOClassDescription) entityMap.get(aName);
		}
		return result;
	}

	/**
	 * Clears all cached class descriptions so that new requests for class
	 * descriptions will be re-loaded on-demand.
	 */
	public static void invalidateClassDescriptionCache() {
		classMap.clear();
		entityMap.clear();
	}

	/**
	 * Registers the specified class descriptiong for the specified class. Nulls are
	 * not allowed - to clear the cache call invalidateClassDescriptionCache().
	 */
	public static void registerClassDescription(EOClassDescription description, Class aClass) {
		if (classMap == null)
			classMap = new HashMap();
		if (entityMap == null)
			entityMap = new HashMap();
		description.theClass = aClass;
		classMap.put(aClass, description);
		entityMap.put(description.entityName(), description);
	}

	/*
	 * public static Object classDelegate() { throw new WotonomyException(
	 * "Not implemented yet." ); }
	 * 
	 * public static void setClassDelegate( Object aDelegate) { throw new
	 * WotonomyException( "Not implemented yet." ); }
	 */

	/**
	 * The string appended to the java class name when searching the class path for
	 * an appropriate description.
	 */
	private final static String ClassNameSuffix = "ClassDesc";

	private static Map classMap;
	private static Map entityMap;

	protected Class theClass;
	private NSMutableArray attributes;

	/**
	 * Constructor may only be called by subclasses.
	 */
	protected EOClassDescription(Class aClass) {
		theClass = aClass;
	}

	/**
	 * Returns a List of all the attributes for this class. This implementation
	 * reflects on the java class to produce a list of attributes, and then removes
	 * those keys that are returned by toOneRelationshipKeys and
	 * toManyRelationhipKeys.
	 */
	public NSArray attributeKeys() {
		if (attributes == null) {
			NSMutableArray readProperties = new NSMutableArray();
			String[] read = Introspector.getReadPropertiesForClass(theClass);
			for (int i = 0; i < read.length; i++) {
				readProperties.addObject(read[i]);
			}

			attributes = new NSMutableArray();
			String[] write = Introspector.getWritePropertiesForClass(theClass);
			for (int i = 0; i < write.length; i++) {
				attributes.addObject(write[i]);
			}

			// only use properties on both lists: read/write
			attributes.retainAll(readProperties);

			// remove relationship keys
			attributes.removeAll(toOneRelationshipKeys());
			attributes.removeAll(toManyRelationshipKeys());
		}
		return attributes;
	}

	/**
	 * This method is called when the specified object has been fetched into the
	 * specified editing context. Fetch means an object was fetched using a fetch
	 * specification - it is not the same thing as an insertion. This implementation
	 * does nothing.
	 */
	public void awakeObjectFromFetch(Object object, EOEditingContext anEditingContext) {
	}

	/**
	 * This method is called when the specified object has been inserted into the
	 * specified editing context. Insertion means an object was inserted by a
	 * display group - it does not mean the same thing as a fetch. This
	 * implementation does nothing.
	 */
	public void awakeObjectFromInsertion(Object object, EOEditingContext anEditingContext) {
		// does nothing
	}

	/**
	 * Returns the class decription for the object referenced by the specified
	 * relationship key, or null if the class description cannot be determined for
	 * that key. This implementation returns null.
	 */
	public EOClassDescription classDescriptionForDestinationKey(String detailKey) {
		return null;
	}

	/**
	 * Creates a new instance of the class represented by this class description,
	 * registering it with the specified editing context and global id. The class
	 * description may not keep references to the newly created object. The editing
	 * context and/or the id may be null. This implementation constructs a new
	 * instance of the class and registers it with the specified editing context. If
	 * the global id is specified, the object will be populated with the appropriate
	 * data, otherwise the object will be treated as a newly inserted object. If no
	 * editing context is specified, the global id is ignored and the new instance
	 * of the class is returned.
	 */
	public Object createInstanceWithEditingContext(EOEditingContext anEditingContext, EOGlobalID globalID) {
//System.out.println( "createInstanceWithEditingContext: " + this + " : " + theClass );
		Object result = null;
		try {
			result = theClass.newInstance();
			if (anEditingContext != null) {
				if (globalID != null) {
					if (result instanceof EOEnterpriseObject) {
						((EOEnterpriseObject) result).awakeFromFetch(anEditingContext);
					}
					// register in editing context
					anEditingContext.recordObject(result, globalID);
				} else // no global id specified
				{
					if (result instanceof EOEnterpriseObject) {
						((EOEnterpriseObject) result).awakeFromInsertion(anEditingContext);
					}
					// register as new object in editing context
					anEditingContext.insertObject(result);
				}
			}
		} catch (Exception exc) {
			// error instantiating
			throw new WotonomyException(exc);
		}
		return result;
	}

	/*
	 * public NSFormatter defaultFormatterForKey( String key ) { throw new
	 * WotonomyException( "Not implemented yet." ); }
	 */

	/**
	 * Returns the delete rule to be used for the specified relationship key. This
	 * implementation returns DeleteRuleNoAction.
	 */
	public int deleteRuleForRelationshipKey(String relationshipKey) {
		return DeleteRuleNoAction;
	}

	/**
	 * Returns a human-readable title for the specified key. For example,
	 * displayNameForKey( "firstName" ) might return "First Name". This
	 * implementation attempts to construct such a string from the key, uppercasing
	 * the first character and inserting spaces before subsequent uppercase
	 * characters.
	 */
	public String displayNameForKey(String key) {
		if (key == null)
			return "";
		if (key.length() == 0)
			return "";

		StringBuffer result = new StringBuffer();
		result.append(Character.toUpperCase(key.charAt(0)));

		char c;
		int len = key.length();
		for (int i = 1; i < len; i++) {
			c = key.charAt(i);
			if (Character.isUpperCase(c)) {
				result.append(' ');
			}
			result.append(c);
		}

		return result.toString();
	}

	/**
	 * Returns a human-readable title for the class of objects that this class
	 * description represents. For example, class CustomerObject might return
	 * "Customer". This implementation returns the class name.
	 */
	public String entityName() {
		String result = theClass.getName();
		int index = result.lastIndexOf(".");
		if (index == -1)
			return result;
		return result.substring(index + 1);
	}

	/**
	 * Returns the fetch specification associated with this class description that
	 * corresponds to the specified name, or null if not found. This implementation
	 * returns null.
	 */
	public EOFetchSpecification fetchSpecificationNamed(String aString) {
		return null;
	}

	/**
	 * Returns the relationship key by which the object at the other end of the
	 * specified relationship key refers to this object, or null if not found. This
	 * implementation returns null.
	 */
	public String inverseForRelationshipKey(String relationshipKey) {
		return null;
	}

	public boolean ownsDestinationObjectsForRelationshipKey(String relationshipKey) {
		throw new WotonomyException("Not implemented yet.");
	}

	/**
	 * Called when this object has been deleted from the specified editing context.
	 * The delete rules for this object's relationships should be executed.
	 */
	public void propagateDeleteForObject(Object object, EOEditingContext anEditingContext) {
		throw new WotonomyException("Not implemented yet.");
	}

	/**
	 * Returns a List of the "to many" relationships for this class. This
	 * implementation returns an empty list.
	 */
	public NSArray toManyRelationshipKeys() {
		return NSArray.EmptyArray;
	}

	/**
	 * Returns a List of the "to one" relationships for this class. This
	 * implementation returns an empty list.
	 */
	public NSArray toOneRelationshipKeys() {
		return NSArray.EmptyArray;
	}

	/**
	 * Returns a human-readable description of the specified object that should not
	 * exceed 60 characters. This implementation returns anObject.toString().
	 */
	public String userPresentableDescriptionForObject(Object anObject) {
		return anObject.toString();
	}

	/**
	 * Verifies that the specified object may be deleted. Throws an exception with a
	 * user-readable error message if the delete operation should not be allowed.
	 * This implementation does nothing.
	 */
	public void validateObjectForDelete(Object object) {
		// does nothing
	}

	/**
	 * Verifies that the specified object may be saved. Throws an exception with a
	 * user-readable error message if the save operation should not be allowed. This
	 * implementation does nothing.
	 */
	public void validateObjectForSave(Object object) {
		// does nothing
	}

	/**
	 * Validates the specified value for the specified key on this this class.
	 * Returns null if the value is acceptable, or returns an object that should be
	 * used in place of the specified object, or throws an exception with a
	 * user-readable error message if no acceptable value can be determined. This
	 * implementation returns null.
	 */
	public Object validateValueForKey(Object value, String key) {
		return null;
	}

	/**
	 * Returns the Java Class that this description describes. NOTE: This method is
	 * not in the specification.
	 */
	public Class getDescribedClass() {
		return theClass;
	}

}

/*
 * $Log$ Revision 1.3 2006/02/18 22:46:44 cgruber Add Surrogate map from .util
 * into control's internal package, and fix imports.
 *
 * Revision 1.2 2006/02/16 16:47:14 cgruber Move some classes in to "internal"
 * packages and re-work imports, etc.
 *
 * Also use UnsupportedOperationExceptions where appropriate, instead of
 * WotonomyExceptions.
 *
 * Revision 1.1 2006/02/16 13:19:57 cgruber Check in all sources in
 * eclipse-friendly maven-enabled packages.
 *
 * Revision 1.11 2003/08/08 05:50:32 chochos theClass is protected instead of
 * private
 *
 * Revision 1.10 2003/08/08 00:37:44 chochos default constructor is needed by
 * subclasses
 *
 * Revision 1.9 2001/12/20 18:55:46 mpowers Hooks for awakeFromInsertion and
 * awakeFromFetch.
 *
 * Revision 1.8 2001/12/01 23:51:45 mpowers Corrected createWithEditingContext.
 *
 * Revision 1.7 2001/11/25 22:43:38 mpowers Corrected
 * createInstanceWithEditingContext.
 *
 * Revision 1.6 2001/04/29 02:29:31 mpowers Debugging relationship faulting.
 *
 * Revision 1.5 2001/04/28 22:17:51 mpowers Revised PropertyDataSource to be
 * EOClassDescription-aware.
 *
 * Revision 1.4 2001/04/28 14:12:23 mpowers Refactored cloning/copying into
 * KeyValueCodingUtilities.
 *
 * Revision 1.3 2001/04/27 23:37:20 mpowers Now using EOClassDescription in the
 * EODataSource class, as we should.
 *
 * Revision 1.2 2001/04/27 00:27:42 mpowers Partial implementation.
 *
 * Revision 1.1 2001/03/29 03:29:49 mpowers Now using KeyValueCoding and Support
 * instead of Introspector.
 *
 *
 */