View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.ui;
20  
21  import net.wotonomy.control.EOClassDescription;
22  import net.wotonomy.control.EODataSource;
23  import net.wotonomy.control.EOEditingContext;
24  import net.wotonomy.control.EOObserverCenter;
25  import net.wotonomy.control.PropertyDataSource;
26  import net.wotonomy.foundation.NSArray;
27  import net.wotonomy.foundation.NSMutableArray;
28  
29  /***
30  * MasterDetailAssociation binds a display group to a property
31  * on the selected object of another display group.
32  * Bindings are: 
33  * <ul>
34  * <li>parent: The property on the selected object of the
35  * bound display group that is expected to be an indexed property.</li> 
36  * </ul>
37  *
38  * @author michael@mpowers.net
39  * @author $Author: cgruber $
40  * @version $Revision: 904 $
41  */
42  public class MasterDetailAssociation extends EOAssociation
43  {
44      static final NSArray aspects = 
45          new NSArray( new Object[] {
46              ParentAspect
47          } );
48      static final NSArray aspectSignatures = 
49          new NSArray( new Object[] {
50              AttributeToOneAspectSignature
51          } );
52      static final NSArray objectKeysTaken = 
53          new NSArray( new Object[] {
54              "allObjects" 
55          } );
56  
57      /***
58      * Used to be notified of changes to objects in the
59      * controlled display group.  requalify() should place 
60      * all objects fetched into the controlled group into
61      * this array.
62      * Otherwise, the parent object is only marked as 
63      * changed for inserts and deletes.
64      */
65      protected NSMutableArray observableArray;
66  		
67      /***
68      * Constructor expecting an EODisplayGroup. 
69      * If the controlled display group does not have a data source,
70      * a new PropertyDataSource will be used.
71      */
72      public MasterDetailAssociation ( Object anObject )
73      {
74          super( anObject );
75          observableArray = new ObservableArray( this );
76      }
77      
78      /***
79      * Returns a List of aspect signatures whose contents
80      * correspond with the aspects list.  Each element is 
81      * a string whose characters represent a capability of
82      * the corresponding aspect. <ul>
83      * <li>"A" attribute: the aspect can be bound to
84      * an attribute.</li>
85      * <li>"1" to-one: the aspect can be bound to a
86      * property that returns a single object.</li>
87      * <li>"M" to-one: the aspect can be bound to a
88      * property that returns multiple objects.</li>
89      * </ul> 
90      * An empty signature "" means that the aspect can
91      * bind without needing a key.
92      * This implementation returns "A1M" for each
93      * element in the aspects array.
94      */
95      public static NSArray aspectSignatures ()
96      {
97          return aspectSignatures;
98      }
99      
100     /***
101     * Returns a List that describes the aspects supported
102     * by this class.  Each element in the list is the string
103     * name of the aspect.  This implementation returns an
104     * empty list.
105     */
106     public static NSArray aspects ()
107     {
108         return aspects;
109     }
110     
111     /***
112     * Returns a List of EOAssociation subclasses that,
113     * for the objects that are usable for this association,
114     * are less suitable than this association.
115     */
116     public static NSArray associationClassesSuperseded ()
117     {
118         return new NSArray();
119     }
120     
121     /***
122     * Returns whether this class can control the specified 
123     * object. 
124     */
125     public static boolean isUsableWithObject ( Object anObject )
126     {
127         return ( anObject instanceof EODisplayGroup );
128     }
129     
130     /***
131     * Returns a List of properties of the controlled object
132     * that are controlled by this class.  For example,
133     * "stringValue", or "selected".
134     */
135     public static NSArray objectKeysTaken ()
136     {
137         return objectKeysTaken;
138     }
139     
140     /***
141     * Returns the aspect that is considered primary
142     * or default.  This is typically "value" or somesuch.
143     */
144     public static String primaryAspect ()
145     {
146         return ParentAspect;
147     }
148         
149     /***
150     * Returns whether this association can bind to the
151     * specified display group on the specified key for
152     * the specified aspect.  
153     */
154     public boolean canBindAspect ( 
155         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
156     {
157         return ( aspects.containsObject( anAspect ) );
158     }
159     
160     /***
161     * Establishes a connection between this association
162     * and the controlled object.  Subclasses should begin
163     * listening for events from their controlled object here.
164     */
165     public void establishConnection ()
166     {
167         //NOTE: if nothing refers to this assocation, it gets gc'd.
168         // otherwise, this is not needed.
169         component().addObserver( this );
170 
171         EODisplayGroup displayGroup = 
172             displayGroupForAspect( ParentAspect );
173         String key = 
174             displayGroupKeyForAspect( ParentAspect );
175 
176         // obtain and qualify new data source from existing source if necessary
177         if ( component().dataSource() == null ) 
178         {
179             if ( ( displayGroup != null ) && ( displayGroup.dataSource() != null ) )
180             {
181                 component().setDataSource( 
182                     displayGroup.dataSource().
183                         dataSourceQualifiedByKey( key ) );
184             }
185         }
186 
187         // set up proxy data source if necessary
188         if ( component().dataSource() == null )
189         {
190             // get context and class desc from master group
191             EOEditingContext editingContext = null;
192             EOClassDescription classDesc = null;
193             if ( displayGroup != null )
194             {
195                 EODataSource dataSource = displayGroup.dataSource();
196                 if ( dataSource != null )
197                 {
198                     editingContext = dataSource.editingContext();
199                     EOClassDescription parentDesc = dataSource.classDescriptionForObjects();
200                     if ( parentDesc != null )
201                     {
202                         classDesc = parentDesc.classDescriptionForDestinationKey( key );   
203                     }
204                 }
205             }
206             
207             //FIXME: should this be called DetailDataSource?
208             component().setDataSource( 
209                new PropertyDataSource( editingContext, classDesc ) );
210         }
211 
212         super.establishConnection();
213 		requalify();
214     }
215     
216     /***
217     * Breaks the connection between this association and 
218     * its object.  Override to stop listening for events
219     * from the object.
220     */
221     public void breakConnection ()
222     {
223         //NOTE: if nothing refers to this assocation, it gets gc'd.
224         // otherwise, this is not needed.
225         component().deleteObserver( this );        
226 
227         super.breakConnection();
228     }
229 	
230     /***
231     * Called when either the selection or the contents 
232     * of an associated display group have changed.
233     */
234     public void subjectChanged ()
235     {
236 		EODisplayGroup displayGroup;
237 
238 		// parent aspect
239 		displayGroup = displayGroupForAspect( ParentAspect );
240 		if ( displayGroup != null )
241 		{
242 			if ( displayGroup.selectionChanged() )
243 			{
244                 requalify();
245 			}
246 			else
247 			if ( displayGroup.contentsChanged() )
248 			{
249                 requalify();
250 			}
251 		}
252     }
253 
254     /***
255     * Overridden to intercept notifications of changes to objects 
256     * in the controlled display group and broadcast a change on the 
257     * parent group's selected object.  All other notifications are
258     * passed to the super implementation.
259     */
260     public void objectWillChange ( Object anObject )
261     {
262         // if child display group is notifying
263         if ( ! ( anObject instanceof EODisplayGroup ) )
264         {
265             // mark parent group's object as changed
266             EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect );
267             if ( displayGroup != null )
268             {
269                 Object selected = displayGroup.selectedObject();
270                 if ( selected != null )
271                 {
272                     // only notify if childrenKey is an attribute of parentDesc
273                     // (and therefore not a toOne or toMany relationship)
274                     EOClassDescription parentDesc = 
275                         EOClassDescription.classDescriptionForClass( 
276                             selected.getClass() );
277                     String key = displayGroupKeyForAspect( ParentAspect );
278                     if ( key != null )
279                     {
280                         int idx = key.indexOf( '.' );
281                         if ( idx != -1 ) key = key.substring( 0, idx );
282                         if ( parentDesc.attributeKeys().contains( key ) )
283                         {
284                             // only notify if we are an attribute key
285                             EOObserverCenter.notifyObserversObjectWillChange( selected );
286                         }
287                     }
288                 }
289             }
290         }
291         else // display group is notifying
292         {
293             // call super so subjectChanged will be called
294             super.objectWillChange( anObject );
295         }
296     }
297     
298     /***
299     * Called by subjectChanged() to requalify the controlled
300     * display group with the selected object and the bound key.
301     */
302 	protected void requalify()
303 	{
304 		EODisplayGroup component = component();
305 		EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect );
306 		String key = displayGroupKeyForAspect( ParentAspect );
307 		
308 		if ( ( displayGroup.selectedObject() != null )
309         && ( component.dataSource() != null ) )
310 		{
311 			component.dataSource().qualifyWithRelationshipKey( 
312 				key, displayGroup.selectedObject() );  
313 			component.fetch();
314             observableArray.setArray( component.allObjects() );
315 		}
316 		else // no selection or no data source, clear
317 		{
318 			component.setObjectArray( null );
319             observableArray.removeAllObjects();
320 		}
321         component.updateDisplayedObjects();
322 	}
323                   
324     /***
325 	* This implementation returns ObserverPrioritySecond
326 	* so that master detail assocations are notified before
327     * other associations.
328     */
329     public int priority ()
330     {
331         return ObserverPrioritySecond;
332     }
333 
334 	// convenience
335 
336     private EODisplayGroup component()
337     {
338         return (EODisplayGroup) object();
339     }
340     
341 }
342 
343 /*
344  * $Log$
345  * Revision 1.2  2006/02/18 23:14:35  cgruber
346  * Update imports and maven dependencies.
347  *
348  * Revision 1.1  2006/02/16 13:22:22  cgruber
349  * Check in all sources in eclipse-friendly maven-enabled packages.
350  *
351  * Revision 1.14  2004/02/04 20:00:49  mpowers
352  * Improved change notification for dotted key paths.
353  *
354  * Revision 1.13  2003/08/06 23:07:52  chochos
355  * general code cleanup (mostly, removing unused imports)
356  *
357  * Revision 1.12  2001/10/26 18:39:05  mpowers
358  * Now a delayed observer with a higher priority, so that it is processed
359  * before other associations.
360  *
361  * Revision 1.11  2001/06/26 21:39:33  mpowers
362  * Added check for null component data source before requalifying.
363  *
364  * Revision 1.10  2001/05/21 14:04:15  mpowers
365  * No longer changing a detail group's data source if it's already specified.
366  *
367  * Revision 1.9  2001/05/18 21:08:46  mpowers
368  * Now calling updateDisplayedObjects on detail after master changes.
369  *
370  * Revision 1.8  2001/05/14 15:26:42  mpowers
371  * Modified logic for controlled groups that have no data source already set.
372  *
373  * Revision 1.7  2001/05/04 14:42:58  mpowers
374  * Now getting stored values in KeyValueCoding.
375  * MasterDetail now marks dirty based on whether it's an attribute
376  * or relation.
377  * Implemented editing context marker.
378  *
379  * Revision 1.6  2001/04/29 02:29:31  mpowers
380  * Debugging relationship faulting.
381  *
382  * Revision 1.4  2001/02/20 16:38:55  mpowers
383  * MasterDetailAssociations now observe their controlled display group's
384  * objects for changes to that the parent object will be marked as updated.
385  * Before, only inserts and deletes to an object's items are registered.
386  * Also, moved ObservableArray to package access.
387  *
388  * Revision 1.3  2001/01/18 16:57:18  mpowers
389  * Fixed problem with losing connection: the association was getting
390  * garbage collected because nothing referred to it.  All other associations
391  * make themselves listeners of their controlled object, and that has been
392  * the only thing keeping them from getting gc'd.  This will need to be fixed.
393  *
394  * Revision 1.2  2001/01/17 23:06:09  mpowers
395  * TreeAssociation now modifies the contents of the children display
396  * group rather than adding items to the titles display group.
397  *
398  * Revision 1.1.1.1  2000/12/21 15:48:23  mpowers
399  * Contributing wotonomy.
400  *
401  * Revision 1.5  2000/12/20 16:25:40  michael
402  * Added log to all files.
403  *
404  *
405  */
406