/* 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.
*
* * 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.
*
* * 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". *
*
* * 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. * * */