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