/* 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.ui; import net.wotonomy.control.EOClassDescription; import net.wotonomy.control.EODataSource; import net.wotonomy.control.EOEditingContext; import net.wotonomy.control.EOObserverCenter; import net.wotonomy.control.PropertyDataSource; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSMutableArray; /** * MasterDetailAssociation binds a display group to a property * on the selected object of another display group. * Bindings are: * * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class MasterDetailAssociation extends EOAssociation { static final NSArray aspects = new NSArray( new Object[] { ParentAspect } ); static final NSArray aspectSignatures = new NSArray( new Object[] { AttributeToOneAspectSignature } ); static final NSArray objectKeysTaken = new NSArray( new Object[] { "allObjects" } ); /** * Used to be notified of changes to objects in the * controlled display group. requalify() should place * all objects fetched into the controlled group into * this array. * Otherwise, the parent object is only marked as * changed for inserts and deletes. */ protected NSMutableArray observableArray; /** * Constructor expecting an EODisplayGroup. * If the controlled display group does not have a data source, * a new PropertyDataSource will be used. */ public MasterDetailAssociation ( Object anObject ) { super( anObject ); observableArray = new ObservableArray( this ); } /** * Returns a List of aspect signatures whose contents * correspond with the aspects list. Each element is * a string whose characters represent a capability of * the corresponding aspect. * An empty signature "" means that the aspect can * bind without needing a key. * This implementation returns "A1M" for each * element in the aspects array. */ public static NSArray aspectSignatures () { return aspectSignatures; } /** * Returns a List that describes the aspects supported * by this class. Each element in the list is the string * name of the aspect. This implementation returns an * empty list. */ public static NSArray aspects () { return aspects; } /** * Returns a List of EOAssociation subclasses that, * for the objects that are usable for this association, * are less suitable than this association. */ public static NSArray associationClassesSuperseded () { return new NSArray(); } /** * Returns whether this class can control the specified * object. */ public static boolean isUsableWithObject ( Object anObject ) { return ( anObject instanceof EODisplayGroup ); } /** * Returns a List of properties of the controlled object * that are controlled by this class. For example, * "stringValue", or "selected". */ public static NSArray objectKeysTaken () { return objectKeysTaken; } /** * Returns the aspect that is considered primary * or default. This is typically "value" or somesuch. */ public static String primaryAspect () { return ParentAspect; } /** * Returns whether this association can bind to the * specified display group on the specified key for * the specified aspect. */ public boolean canBindAspect ( String anAspect, EODisplayGroup aDisplayGroup, String aKey) { return ( aspects.containsObject( anAspect ) ); } /** * Establishes a connection between this association * and the controlled object. Subclasses should begin * listening for events from their controlled object here. */ public void establishConnection () { //NOTE: if nothing refers to this assocation, it gets gc'd. // otherwise, this is not needed. component().addObserver( this ); EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect ); String key = displayGroupKeyForAspect( ParentAspect ); // obtain and qualify new data source from existing source if necessary if ( component().dataSource() == null ) { if ( ( displayGroup != null ) && ( displayGroup.dataSource() != null ) ) { component().setDataSource( displayGroup.dataSource(). dataSourceQualifiedByKey( key ) ); } } // set up proxy data source if necessary if ( component().dataSource() == null ) { // get context and class desc from master group EOEditingContext editingContext = null; EOClassDescription classDesc = null; if ( displayGroup != null ) { EODataSource dataSource = displayGroup.dataSource(); if ( dataSource != null ) { editingContext = dataSource.editingContext(); EOClassDescription parentDesc = dataSource.classDescriptionForObjects(); if ( parentDesc != null ) { classDesc = parentDesc.classDescriptionForDestinationKey( key ); } } } //FIXME: should this be called DetailDataSource? component().setDataSource( new PropertyDataSource( editingContext, classDesc ) ); } super.establishConnection(); requalify(); } /** * Breaks the connection between this association and * its object. Override to stop listening for events * from the object. */ public void breakConnection () { //NOTE: if nothing refers to this assocation, it gets gc'd. // otherwise, this is not needed. component().deleteObserver( this ); super.breakConnection(); } /** * Called when either the selection or the contents * of an associated display group have changed. */ public void subjectChanged () { EODisplayGroup displayGroup; // parent aspect displayGroup = displayGroupForAspect( ParentAspect ); if ( displayGroup != null ) { if ( displayGroup.selectionChanged() ) { requalify(); } else if ( displayGroup.contentsChanged() ) { requalify(); } } } /** * Overridden to intercept notifications of changes to objects * in the controlled display group and broadcast a change on the * parent group's selected object. All other notifications are * passed to the super implementation. */ public void objectWillChange ( Object anObject ) { // if child display group is notifying if ( ! ( anObject instanceof EODisplayGroup ) ) { // mark parent group's object as changed EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect ); if ( displayGroup != null ) { Object selected = displayGroup.selectedObject(); if ( selected != null ) { // only notify if childrenKey is an attribute of parentDesc // (and therefore not a toOne or toMany relationship) EOClassDescription parentDesc = EOClassDescription.classDescriptionForClass( selected.getClass() ); String key = displayGroupKeyForAspect( ParentAspect ); if ( key != null ) { int idx = key.indexOf( '.' ); if ( idx != -1 ) key = key.substring( 0, idx ); if ( parentDesc.attributeKeys().contains( key ) ) { // only notify if we are an attribute key EOObserverCenter.notifyObserversObjectWillChange( selected ); } } } } } else // display group is notifying { // call super so subjectChanged will be called super.objectWillChange( anObject ); } } /** * Called by subjectChanged() to requalify the controlled * display group with the selected object and the bound key. */ protected void requalify() { EODisplayGroup component = component(); EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect ); String key = displayGroupKeyForAspect( ParentAspect ); if ( ( displayGroup.selectedObject() != null ) && ( component.dataSource() != null ) ) { component.dataSource().qualifyWithRelationshipKey( key, displayGroup.selectedObject() ); component.fetch(); observableArray.setArray( component.allObjects() ); } else // no selection or no data source, clear { component.setObjectArray( null ); observableArray.removeAllObjects(); } component.updateDisplayedObjects(); } /** * This implementation returns ObserverPrioritySecond * so that master detail assocations are notified before * other associations. */ public int priority () { return ObserverPrioritySecond; } // convenience private EODisplayGroup component() { return (EODisplayGroup) object(); } } /* * $Log$ * Revision 1.2 2006/02/18 23:14:35 cgruber * Update imports and maven dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber * Check in all sources in eclipse-friendly maven-enabled packages. * * Revision 1.14 2004/02/04 20:00:49 mpowers * Improved change notification for dotted key paths. * * Revision 1.13 2003/08/06 23:07:52 chochos * general code cleanup (mostly, removing unused imports) * * Revision 1.12 2001/10/26 18:39:05 mpowers * Now a delayed observer with a higher priority, so that it is processed * before other associations. * * Revision 1.11 2001/06/26 21:39:33 mpowers * Added check for null component data source before requalifying. * * Revision 1.10 2001/05/21 14:04:15 mpowers * No longer changing a detail group's data source if it's already specified. * * Revision 1.9 2001/05/18 21:08:46 mpowers * Now calling updateDisplayedObjects on detail after master changes. * * Revision 1.8 2001/05/14 15:26:42 mpowers * Modified logic for controlled groups that have no data source already set. * * Revision 1.7 2001/05/04 14:42:58 mpowers * Now getting stored values in KeyValueCoding. * MasterDetail now marks dirty based on whether it's an attribute * or relation. * Implemented editing context marker. * * Revision 1.6 2001/04/29 02:29:31 mpowers * Debugging relationship faulting. * * Revision 1.4 2001/02/20 16:38:55 mpowers * MasterDetailAssociations now observe their controlled display group's * objects for changes to that the parent object will be marked as updated. * Before, only inserts and deletes to an object's items are registered. * Also, moved ObservableArray to package access. * * Revision 1.3 2001/01/18 16:57:18 mpowers * Fixed problem with losing connection: the association was getting * garbage collected because nothing referred to it. All other associations * make themselves listeners of their controlled object, and that has been * the only thing keeping them from getting gc'd. This will need to be fixed. * * Revision 1.2 2001/01/17 23:06:09 mpowers * TreeAssociation now modifies the contents of the children display * group rather than adding items to the titles display group. * * Revision 1.1.1.1 2000/12/21 15:48:23 mpowers * Contributing wotonomy. * * Revision 1.5 2000/12/20 16:25:40 michael * Added log to all files. * * */