/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 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.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.TreeSet; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.internal.Introspector; import net.wotonomy.foundation.internal.WotonomyException; /** * A data source that reads and writes to an indexed property of a java object. * This class is used by MasterDetailAssociation to retreive objects from the * master display group. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 894 $ */ public class PropertyDataSource extends OrderedDataSource { protected Object source; protected String key; protected Class lastKnownType; // for best-guessing protected EOClassDescription classDesc; protected EOEditingContext context; /** * Creates a new PropertyDataSource with no editing context and will try to * guess the appropriate class description when trying to create objects. */ public PropertyDataSource() { this(null, (EOClassDescription) null); } /** * Creates a new PropertyDataSource that uses the specified editing context, but * will try to guess the appropriate class description when trying to create * objects. */ public PropertyDataSource(EOEditingContext aContext) { this(aContext, (EOClassDescription) null); } /** * Creates a new PropertyDataSource that uses the specified editing context and * vends objects of the specified class. */ public PropertyDataSource(EOEditingContext aContext, Class aClass) { this(aContext, EOClassDescription.classDescriptionForClass(aClass)); } /** * Creates a new PropertyDataSource that uses the specified editing context and * vends objects of the specified class description. */ public PropertyDataSource(EOEditingContext aContext, EOClassDescription aClassDesc) { source = null; key = null; lastKnownType = null; classDesc = aClassDesc; context = aContext; } /** * Provides the master object for detail display groups. */ public Object source() { return source; } /** * Allows a detail display group to set the master object. */ public void setSource(Object anObject) { source = anObject; } /** * Provides the detail key for detail display groups. */ public String key() { return key; } /** * Allows a detail display group to set the detail key. */ public void setKey(String aKey) { key = aKey; } /** * Inserts the specified object into this data source. Calls insertObjectAtIndex * and appends to the end of the list. */ public void insertObject(Object anObject) { insertObjectAtIndex(anObject, -1); // trick to force to end } /** * Inserts the specified object into this data source, at the specified index. */ public void insertObjectAtIndex(Object anObject, int anIndex) { if (source == null) return; List list = readAsList(); if (anIndex == -1) anIndex = list.size(); // force to end if (anIndex > list.size()) anIndex = list.size(); // force to end list.add(anIndex, anObject); writeAsList(list); } /** * Deletes the specified object from this data source. */ public void deleteObject(Object anObject) { if (source == null) return; List list = readAsList(); list.remove(anObject); writeAsList(list); } public EOEditingContext editingContext() { return context; } /** * Returns a List containing the objects in this data source. */ public NSArray fetchObjects() { if (source == null) return NSArray.EmptyArray; return readAsList(); } /** * Returns a new instance of this class. */ public EODataSource dataSourceQualifiedByKey(String aKey) { // determine the target class desc if possible EOClassDescription keyClassDesc = null; if (classDesc != null) { keyClassDesc = classDesc.classDescriptionForDestinationKey(aKey); } return new PropertyDataSource(editingContext(), keyClassDesc); } /** * Restricts this data source to vend those objects that are associated with the * specified key on the specified object. */ public void qualifyWithRelationshipKey(String aKey, Object anObject) { source = anObject; key = aKey; } /** * Returns the class description passed to the constructor, if any. If no class * description and if the bound property is an indexed property, the type of the * array is returned, otherwise this method returns null. This method is called * by createObject(). */ public EOClassDescription classDescriptionForObjects() { // just return the class description if we have one if (classDesc != null) return classDesc; // otherwise, try to do some guesswork EOClassDescription result = null; // lastKnownType is not updated here Class type = lastKnownType; // if no last known type if (type == null) { // if source and key were specified if ((source != null) && (key != null)) { // try to get an array type Method m = Introspector.getPropertyReadMethod(source.getClass(), key, new Class[0]); if (m != null) { Class returnType = m.getReturnType(); if (returnType.isArray()) { type = returnType.getComponentType(); } } else { throw new WotonomyException("Key does not exist for object: " + key + " : " + source); } } // does not update lastKnownType because // we prefer to get that info from a fetch. } // if type has been determined if (type != null) { result = EOClassDescription.classDescriptionForClass(type); } return result; } /** * Calls getValue() and returns the result as a List. Sets lastKnownType to the * retrieved type. */ protected NSMutableArray readAsList() { Object value = getValue(); if (value == null) { return new NSMutableArray(); } Object o; NSMutableArray result = new NSMutableArray(); boolean hasReadType = false; lastKnownType = null; // if instance of array, convert to list if (value.getClass().isArray()) { int count = Array.getLength(value); for (int i = 0; i < count; i++) { o = Array.get(value, i); if (o != null) { // we've already found a type if (hasReadType) { // check that this matches the last known type if (o.getClass() != lastKnownType) { // not all of the same type: set to null lastKnownType = null; } } else // this is the first type we've found { // remember it hasReadType = true; lastKnownType = o.getClass(); } } result.add(o); } } else if (value instanceof Collection) { // convert to list so we handle sets, etc. Iterator i = ((Collection) value).iterator(); while (i.hasNext()) { o = i.next(); if (o != null) { // we've already found a type if (hasReadType) { // check that this matches the last known type if (o.getClass() != lastKnownType) { // not all of the same type: set to null lastKnownType = null; } } else // this is the first type we've found { // remember it hasReadType = true; lastKnownType = o.getClass(); } } result.add(o); } } else { lastKnownType = null; throw new WotonomyException("PropertyDataSource: " + "bound property was not an indexed property: " + key); } return result; } /** * Converts the specified List to lastKnownType and calls setValue(). */ protected void writeAsList(List anObjectList) { if (source == null) { throw new WotonomyException("PropertyDataSource: " + "no source object: " + key); } Class c = source.getClass(); Method m = Introspector.getPropertyReadMethod(c, key, new Class[0]); if (m == null) { throw new WotonomyException("Could not read property for object: " + key + " : " + source + " (" + c + ")"); } Class returnType = m.getReturnType(); int count = anObjectList.size(); Object result = null; if (returnType.isArray()) { Class type = returnType.getComponentType(); result = Array.newInstance(type, count); for (int i = 0; i < count; i++) { Array.set(result, i, anObjectList.get(i)); } } else { Collection collection = null; if (!returnType.isInterface()) { try { collection = (Collection) returnType.newInstance(); } catch (Exception exc) { // no default constructor, leave null } } // try to find an acceptable collections type if (collection == null) { if (returnType.isAssignableFrom(NSMutableArray.class)) { collection = new NSMutableArray(); } else if (returnType.isAssignableFrom(LinkedList.class)) { collection = new LinkedList(); } else if (returnType.isAssignableFrom(ArrayList.class)) { collection = new ArrayList(); } else if (returnType.isAssignableFrom(HashSet.class)) { collection = new HashSet(); } else if (returnType.isAssignableFrom(TreeSet.class)) { collection = new TreeSet(); } } if (collection == null) { throw new WotonomyException("Could not create a collection of type: " + returnType); } collection.addAll(anObjectList); result = collection; } setValue(result); } /** * Returns the value of the indexed property specified by * qualifyWithRelationshipKey. */ protected Object getValue() { if (source instanceof EOKeyValueCoding) { return ((EOKeyValueCoding) source).valueForKey(key); } return EOKeyValueCodingSupport.valueForKey(source, key); } /** * Sets the value of the indexed property specified by * qualifyWithRelationshipKey. The argument is assumed to be of appropriate type * for the property. EOObserverCenter is notified that the object will change. */ protected void setValue(Object aValue) { EOClassDescription sourceDesc = EOClassDescription.classDescriptionForClass(source.getClass()); // if we're not editing a relationship (?) // if ( ! sourceDesc.toManyRelationshipKeys().containsObject( key ) ) { // mark the parent as changed EOObserverCenter.notifyObserversObjectWillChange(source); } if (source instanceof EOKeyValueCoding) { ((EOKeyValueCoding) source).takeValueForKey(aValue, key); } else { EOKeyValueCodingSupport.takeValueForKey(source, aValue, key); } } } /* * $Log$ 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.14 2003/01/18 23:30:42 mpowers WODisplayGroup now compiles. * * Revision 1.13 2002/10/24 21:15:36 mpowers New implementations of NSArray and * subclasses. * * Revision 1.12 2002/10/24 18:18:12 mpowers NSArray's are now considered * read-only, so we can return our internal representation to reduce unnecessary * object allocation. * * Revision 1.11 2002/04/15 21:55:33 mpowers Catching a condition where the get * may not return the value passed to set. * * Revision 1.10 2002/03/08 23:20:37 mpowers insertObject now calls * insertObjectAtIndex. * * Revision 1.9 2001/06/05 19:10:41 mpowers Better handling of null properties. * * Revision 1.8 2001/05/21 14:03:35 mpowers Added a convenience constructor for * java classes. * * Revision 1.7 2001/04/30 13:15:24 mpowers Child contexts re-initializing * objects invalidated in parent now propery transpose relationships. * * 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/27 23:37:20 mpowers Now using EOClassDescription in the * EODataSource class, as we should. * * Revision 1.3 2001/03/29 03:29:49 mpowers Now using KeyValueCoding and Support * instead of Introspector. * * Revision 1.2 2001/01/24 14:10:53 mpowers Contributing OrderedDataSource, and * PropertyDataSource extends it. * * Revision 1.1.1.1 2000/12/21 15:46:50 mpowers Contributing wotonomy. * * Revision 1.3 2000/12/20 16:25:35 michael Added log to all files. * * */