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