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