View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.trinidad.change;
20  
21  import javax.faces.component.NamingContainer;
22  import javax.faces.component.UIComponent;
23  import javax.faces.context.FacesContext;
24  
25  import org.apache.myfaces.trinidad.component.UIXComponent;
26  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
27  import org.apache.myfaces.trinidad.util.ComponentUtils;
28  
29  import org.w3c.dom.Node;
30  
31  
32  /**
33   * Change specialization for moving a child from one container to another.
34   * MoveChildComponent should be registered on a parent component that is
35   * common to the child being moved and the container component at destination.
36   * In other words, while calling addComponentChange() or addDocumentChange()
37   * methods on the ChangeManager to add a MoveChildComponentChange, the common
38   * parent component instance must be passed as an argument. The add() utility
39   * method in this class can be alternatively used to conveniently register the
40   * change against the common parent. While applying this change, if a child with
41   * the same id as the movable child were to be already present in the destination
42   * container, the move operation is aborted.
43   * @see #add(FacesContext, ChangeManager)
44   * @see ChangeManager#addComponentChange(FacesContext, UIComponent, ComponentChange)
45   * @see ChangeManager#addDocumentChange(FacesContext, UIComponent, DocumentChange)
46   */
47  public final class MoveChildComponentChange 
48    extends ComponentChange
49    implements DocumentChange
50  {
51    /**
52     * Constructs a MoveChildComponentChange. The child will be appended to the
53     * list of children of the destinationContainer.
54     * @param movableChild The child component to be moved.
55     * @param destinationContainer The destination component into which the child
56     * component is to be moved.
57     * @throws IllegalArgumentException If movableChild or destinationContainer
58     * is null
59     */
60    public MoveChildComponentChange(
61      UIComponent movableChild,
62      UIComponent destinationContainer)
63    {
64      this(movableChild, destinationContainer, null);
65    }
66    
67    /**
68     * Constructs a MoveChildComponentChange. The child will be inserted to the 
69     * list of children of the destinationContainer, before the supplied 
70     * insertBeforecomponent. If the supplied insertBeforeComponent is null, the 
71     * child will be appended to the list of children of the destinationContainer.
72     * If the insertBeforeComponent is non-null, and if it were not to be found
73     * while applying this change, the movableChild will not be moved.
74     * @param movableChild The child component to be moved.
75     * @param destinationContainer The destination component into which the child 
76     * component is to be moved. This should not be null if the insertBeforeComponent
77     * is null.
78     * @param insertBeforeComponent The component before which the moved child is
79     * to be inserted. This can be null, in which case the movableChild is
80     * appended.
81     * @throws IllegalArgumentException If movableChild is null or destinationContainer
82     * and insertBeforeComponent is null, or if a parent component common to 
83     * movableChild and destinationContainer could not be found.
84     */
85    public MoveChildComponentChange(
86      UIComponent movableChild,
87      UIComponent destinationContainer, 
88      UIComponent insertBeforeComponent)
89    {
90      if (movableChild == null)
91      {
92        throw new IllegalArgumentException(_LOG.getMessage("MOVABLE_CHILD_REQUIRED"));
93      }
94      
95      /////////////////////////////
96      
97      FacesContext context = FacesContext.getCurrentInstance();
98  
99      // get the doc paths first and validate
100     _moveCompDocPath = ComponentUtils.getDocumentLocationForComponent(context, movableChild);
101     _destinationContainerDocPath = 
102       ComponentUtils.getDocumentLocationForComponent(context, destinationContainer);
103 
104     if (_moveCompDocPath == null || _destinationContainerDocPath == null)
105     {
106       // if either components are not in a doc, component is not in the tree, error condition
107       throw new IllegalArgumentException(
108         _LOG.getMessage("NO_CONTAINING_DOC_FOUND", 
109                         (_moveCompDocPath == null) ? movableChild : destinationContainer));
110     }
111     
112     /////////////////////////////
113 
114     // validate destination container
115     destinationContainer = _getValidatedDestinationContainer(destinationContainer, 
116                                                              insertBeforeComponent);
117     // find and validate the common parent
118     UIComponent commonParent = _getValidatedCommonParent(context, movableChild, destinationContainer);
119     _commonParentDocPath = ComponentUtils.getDocumentLocationForComponent(context, commonParent);
120     
121     /////////////////////////////
122     
123     UIComponent viewRoot = context.getViewRoot();
124 
125     // Get the scoped id's for move participants (scoped / relative to common parent)
126     _moveCompScopedIdAtSource = ComponentUtils.getScopedIdForComponent(movableChild, commonParent);
127     _moveCompParentScopedId = 
128       ComponentUtils.getScopedIdForComponent(movableChild.getParent(), commonParent);
129     _destinationContainerScopedId = ComponentUtils.getScopedIdForComponent(destinationContainer,
130                                                                            commonParent);
131     _commonParentScopedId = ComponentUtils.getScopedIdForComponent(commonParent, viewRoot);
132     
133     // cannot proceed if we could not get id for the move participants
134     if (_moveCompScopedIdAtSource == null || 
135         _moveCompParentScopedId == null || 
136         _destinationContainerScopedId == null ||
137         _commonParentScopedId == null)
138     {
139       throw new IllegalArgumentException(_LOG.getMessage("MOVE_PARTICIPANTS_WITHOUT_ID"));
140     }
141     
142     /////////////////////////////
143 
144     // get the id path upto the naming container of the common parent so that we can compute the 
145     //  absolute scoped ids from the scoped ids
146     String commonParentPrefix = _getScopedIdPrefix(commonParent, _commonParentScopedId);
147       
148     // calculate the absolute scoped ids (scoped from ViewRoot) for the movable componen at its 
149     //  source so that we can handle remapping scoped ids in the SessionChangeManager    
150     _moveCompAbsoluteScopedIdAtSource = (commonParentPrefix != null) ?
151                                           new StringBuilder(commonParentPrefix).
152                                             append(NamingContainer.SEPARATOR_CHAR).
153                                             append(_moveCompScopedIdAtSource).toString()
154                                           : _moveCompScopedIdAtSource;
155     
156     // find the logical (id in context of the document where the component is defined) scoped id 
157     _moveCompAbsoluteLogicalScopedIdAtSource = 
158       ComponentUtils.getLogicalScopedIdForComponent(movableChild, viewRoot);
159     
160     /////////////////////////////
161     
162     // calculate the absolute scoped ids of the moveable component at destination after move
163     String destinationContainerPrefix = _getScopedIdPrefix(destinationContainer, 
164                                                            _destinationContainerScopedId);
165     StringBuilder moveCompAtDestinationScopedIdBuilder = new StringBuilder();
166     
167     if (commonParentPrefix != null)
168     {
169       moveCompAtDestinationScopedIdBuilder.append(commonParentPrefix).
170         append(NamingContainer.SEPARATOR_CHAR);
171     }
172     
173     if (destinationContainerPrefix != null)
174     {
175       moveCompAtDestinationScopedIdBuilder.append(destinationContainerPrefix).
176         append(NamingContainer.SEPARATOR_CHAR);
177     }
178     
179     String movableChildId = movableChild.getId();
180     _moveCompAbsoluteScopedIdAtDestination = 
181       moveCompAtDestinationScopedIdBuilder.append(movableChildId).toString();
182     String destinationLogicalPrefix = 
183       _getScopedIdPrefix(destinationContainer,
184                          ComponentUtils.getLogicalScopedIdForComponent(destinationContainer, 
185                                                                        viewRoot));
186     
187     // find the logical id
188     _moveCompAbsoluteLogicalScopedIdAtDestination = (destinationLogicalPrefix != null) ? 
189                                                       new StringBuilder(destinationLogicalPrefix).
190                                                         append(NamingContainer.SEPARATOR_CHAR).
191                                                         append(movableChildId).toString()
192                                                       : movableChildId;
193 
194     /////////////////////////////
195 
196     // For insertBeforeComponent, we do not care to obtain scoped id.
197     _insertBeforeCompId = (insertBeforeComponent == null) ? null : insertBeforeComponent.getId();
198   }
199   
200   /**
201    * Convenience method to add this MoveChildComponentChange to the supplied
202    * ChangeManager. The change will be registered against a parent component
203    * that is common to the child being moved and the container component at
204    * destination.
205    * @param facesContext The FacesContext instance for the current request
206    * @param changeManager The ChangeManager instance on which this
207    * MoveChildComponentChange is to be added.
208    * @return The common parent component against which this 
209    * MoveChildComponentChange was registered.
210    */
211   public UIComponent add(
212     FacesContext facesContext, 
213     ChangeManager changeManager) 
214   {
215     UIComponent commonParent = facesContext.getViewRoot().findComponent(_commonParentScopedId);
216     
217     if (commonParent == null)
218     {
219       _LOG.warning("COMMON_PARENT_NOT_FOUND", _commonParentScopedId);
220       return null;
221     }
222     
223     // Register a move change against the common parent
224     changeManager.addComponentChange(facesContext, commonParent, this);
225     
226     return commonParent;
227   }
228    
229   /**
230    * Apply this change to the specified component.
231    * @param changeTargetComponent The component that is a common parent to the 
232    * movable child and the destination container.
233    * @throws IllegalArgumentException If the supplied changeTargetComponent
234    * is null.
235    */
236   @Override
237   public void changeComponent(UIComponent changeTargetComponent)
238   {
239     if (changeTargetComponent == null)
240       throw new IllegalArgumentException(
241         _LOG.getMessage("COMPONENT_REQUIRED"));
242     
243     // 1. Check for destination container component 
244     UIComponent destinationContainer = 
245       changeTargetComponent.findComponent(_destinationContainerScopedId);
246     if(destinationContainer == null)
247     {
248       _LOG.warning("DESTINATION_CONTAINER_NOT_FOUND", _destinationContainerScopedId);
249       return;
250     }
251     
252     // 2. Find movableChild, gather the possible duplicates (theoritically only three) and keep a
253     //  single copy among them.
254     //  Duplicates are possible because 
255     //  a) taghandlers re-create the component that was in the jspx file in
256     //    their original location, no matter whether it was moved/removed due to 
257     //    aplication of a different ComponentChange. Such components could now 
258     //    be considered duplicates, because they are newly created from their vanilla definition
259     //    in the jspx document, and would not have any further ComponentChanges applied on them. 
260     //    There should be one such duplicate.
261     //  b) Apart from adding the MoveComponentChange, we expect developers to apply the change in 
262     //    order to reflect in the same request cycle (i.e. developers call 
263     //    ComponentChange.changeComponent()). Consequently, the component tree contains a moved 
264     //    child at the destination. Such components must be preserved, because they have 
265     //    incorporated any subsequent ComponentChanges on them. There should be one such moved
266     //    component.
267     //  c) We would have moved/added components due to previous customization an earlier application 
268     //    of ComponentChange, that could still be in the view tree. There should be one such zombie/ 
269     //    duplicate.
270     UIComponent sourceParent = 
271       changeTargetComponent.findComponent(_moveCompParentScopedId);
272     
273     UIComponent foundChild = 
274       changeTargetComponent.findComponent(_moveCompScopedIdAtSource);
275 
276     // To flag if a child was already found in a destination container (maybe due to previous move)    
277     boolean isChildIdAtDestination = false;
278     
279     UIComponent movableChild = null;
280     int movableChildIndex = 0;
281     UIComponent movedChild = null;
282     int movedChildIndex = 0;
283     UIComponent duplicateChild = null;
284     int duplicateChildIndex = 0;
285     UIComponent duplicateChildParent = null;
286 
287     while (foundChild != null)
288     {
289       // 2.a. If the parent matches, this could be the component that JSF-Runtime re-created
290       //  and added because it is in the physical document
291       if (foundChild.getParent().equals(sourceParent))
292       {
293         movableChild = foundChild;
294         movableChildIndex = sourceParent.getChildren().indexOf(movableChild);
295       }
296       // 2.b.a. We could possibly find the child at its destination, because apart from
297       //  adding the change, the change was applied in previous request, and the move
298       //  could have been within the same naming container umbrella. In this case
299       //  we do not want to move anything and the movable child is considered as a 
300       //  duplicate and candidate for removal.
301       else if (foundChild.getParent().equals(destinationContainer))
302       {
303         isChildIdAtDestination = true;
304         movedChild = foundChild;
305         movedChildIndex = destinationContainer.getChildren().indexOf(movedChild);
306       }
307       // 2.c. Possible dup from subsequent MoveChildComponentChange in the sequence of multiple
308       //  moves of the component in this same request. For example, if the move is from A->B->C,
309       //  and if we are currently dealing with move from A->B, the component that was added at
310       //  position C (in addition to adding the move change to changemanager) will now be dup.
311       else
312       {
313         duplicateChild = foundChild;
314         duplicateChildIndex = foundChild.getParent().getChildren().indexOf(foundChild);
315         duplicateChildParent = foundChild.getParent();
316       }
317 
318       // Invariably, remove the found component from the tree. We remove the
319       //  movableChild also, otherwise, findComponent blind loops on this same 
320       //  component if movableChild and duplicates are within same immediate
321       //  NamingContainer.
322       foundChild.getParent().getChildren().remove(foundChild);
323 
324       // Try and find the next potential copy of the component to move
325       foundChild = changeTargetComponent.findComponent(_moveCompScopedIdAtSource);
326     }
327     
328     //  We need to re-attach the dup for now, the dupes will be eliminated gradually while applying
329     //  the successive move change involving the same component.
330     if (duplicateChild != null)
331     {
332       duplicateChildParent.getChildren().add(duplicateChildIndex, duplicateChild);
333     }
334 
335     // Can't do anything without a movable child.    
336     if(movableChild == null)
337     {
338       _LOG.warning("MOVABLE_CHILD_NOT_FOUND", _moveCompScopedIdAtSource);
339 
340       // Reverse any damage that we might have caused, and exit
341       if (movedChild != null)
342       {
343         destinationContainer.getChildren().add(movedChildIndex, movedChild);
344       }
345       return;
346     }
347     
348     // 2.b.b. Similar to situation in step #2.b.a, but here the move is across different naming 
349     //  containers, we could not catch this earlier.
350     if (!isChildIdAtDestination)
351     {
352       String movableChildId = movableChild.getId();
353       for (UIComponent childComponent:destinationContainer.getChildren())
354       {
355         if (movableChildId.equals(childComponent.getId()))
356         {
357           isChildIdAtDestination = true;
358           movedChild = childComponent;
359           // Temporarily remove this child, we might add it back in step #3 below.
360           movedChild.getParent().getChildren().remove(movedChild);
361           break;
362         }
363       }
364     }
365 
366     // 3. Check whether the destination container has a child with same id.
367     if (isChildIdAtDestination)
368     {
369       _LOG.warning("MOVABLE_CHILD_SAME_ID_FOUND", _moveCompScopedIdAtSource);
370 
371       // Component type matches, this means the child is already at destination. We have removed all
372       //  duplicates, and have nothing more to do in this case
373       if ( (movableChild.getFamily().equals(movedChild.getFamily())) &&
374              (movableChild.getRendererType().equals(movedChild.getRendererType())) )
375       {
376         // Add back the moved child that we removed earlier.
377         destinationContainer.getChildren().add(movedChildIndex, movedChild);
378       }
379       else
380       {
381         // Duplicate child by id, but not of the same component type - a condition we cannot handle.
382         // Reverse any damage that we might have caused and exit
383         sourceParent.getChildren().add(movableChildIndex, movableChild);
384       }
385       return;
386     }
387 
388     // We are now dealing with case where there were no duplicates, and a proper point-to-point
389     //  move should happen. Reattach the moveable child, so that move happens atomically at the end.
390     sourceParent.getChildren().add(movableChildIndex, movableChild);
391     
392     // 4. See if we can find the insertBeforeComponent among the destinationContainer's children
393     int insertIndex = -1;
394     if (_insertBeforeCompId != null)
395     {
396       for (UIComponent childComponent:destinationContainer.getChildren())
397       {
398         if (_insertBeforeCompId.equals(childComponent.getId()))
399         {
400           insertIndex = 
401             destinationContainer.getChildren().indexOf(childComponent);
402           break;
403         }
404       }
405   
406       // insertBeforeId was specified, but we cannot find the insertBefore component. Exit.
407       if (insertIndex == -1)
408       {
409         _LOG.warning("INSERT_BEFORE_NOT_FOUND", _insertBeforeCompId);
410         return;
411       }
412     }
413     
414     // 5. Atomically move the child
415     if (insertIndex == -1)
416       destinationContainer.getChildren().add(movableChild);
417     else
418       destinationContainer.getChildren().add(insertIndex, movableChild);
419   }
420   
421   /**
422    * Given the DOM Node representing a Component, apply any necessary
423    * DOM changes. The node passed will be the Node that is a common parent for
424    * the movable child and the destination container.
425    * There is a limitation with the document change, that the movable child 
426    * Node, destination container Node, and the common parent Node have to belong
427    * to the same document.
428    * @param changeTargetNode DOM Node that is a common parent for the movable
429    * child and the destination container.
430    * @throws IllegalArgumentException If changeTargeNode were to be null.
431    */
432   public void changeDocument(Node changeTargetNode)
433   {
434     if (changeTargetNode == null)
435       throw new IllegalArgumentException(_LOG.getMessage("NO_NODE_SPECIFIED"));
436 
437     if ( !_moveCompDocPath.equals(_destinationContainerDocPath) ||
438          !_moveCompDocPath.equals(_commonParentDocPath) )
439     {
440       // If all three participants are not in same doc, we cannot proceed with appling doc change.
441       // Throw an exception so that ChangeManagers can handle this failure and do alternate
442       //  processing (eg. add a component change given that doc change failed)
443       throw new IllegalStateException(
444         _LOG.getMessage("MOVE_PARTICIPANTS_NOT_IN_SAME_DOC", 
445                         new Object[] {_moveCompDocPath,
446                                       _destinationContainerDocPath,
447                                       _commonParentDocPath}));
448     }
449 
450     // Move involves four steps.
451     // 1. Finding the child node, the source of move
452     Node movableChildNode = 
453       ChangeUtils.__findNodeByScopedId(changeTargetNode, 
454                                        _moveCompScopedIdAtSource, 
455                                        Integer.MAX_VALUE);
456     
457     if(movableChildNode == null)
458     {
459       _LOG.warning("MOVABLE_CHILD_NOT_FOUND", _moveCompScopedIdAtSource);
460       return;
461     }
462     
463     // 2. Finding the destination container node
464     Node destinationContainerNode = 
465       ChangeUtils.__findNodeByScopedId(changeTargetNode, 
466                                        _destinationContainerScopedId, 
467                                        Integer.MAX_VALUE);
468 
469     
470     if(destinationContainerNode == null)
471     {
472       _LOG.warning("DESTINATION_CONTAINER_NOT_FOUND", _destinationContainerScopedId);
473       return;
474     }
475     
476     //3. Finding the neighbor at the destination
477     Node insertBeforeNode = (_insertBeforeCompId == null) ? 
478       null:ChangeUtils.__findNodeByScopedId(destinationContainerNode, 
479                                             _insertBeforeCompId, 
480                                             1);
481     // insertBeforeId was specified, but corresponding component is missing.
482     //  Abort the move.
483     if(_insertBeforeCompId != null && insertBeforeNode == null)
484     {
485       _LOG.warning("INSERT_BEFORE_NOT_FOUND", _insertBeforeCompId);
486       return;
487     }
488 
489     //4. Atomically move the child.
490     destinationContainerNode.insertBefore(movableChildNode, insertBeforeNode);
491   }
492 
493   /** 
494    * Returns true if adding the DocumentChange should force the JSP Document
495    * to reload
496    * @return true Since moving of components should force the document to reload
497    */
498   public boolean getForcesDocumentReload()
499   {
500     return true;
501   }
502   
503   /**
504    * Returns the absolute scopedId (relative to the ViewRoot) of the movable component as it is 
505    *  before the move
506    */
507   public String getSourceScopedId()
508   {
509     return _moveCompAbsoluteScopedIdAtSource;
510   }
511 
512   /**
513    * Returns the absolute scopedId (relative to the ViewRoot) of the movable component as it would 
514    *  be after the move
515    */
516   public String getDestinationScopedId()
517   {
518     return _moveCompAbsoluteScopedIdAtDestination;
519   }
520   
521   /**
522    * Returns the absolute logical scopedId of the movable component as it is before the move. 
523    * 
524    * The id returned here will be in context of the document where the component is defined. For 
525    *  example, consider a component that is defined in a base document and is relocated to a 
526    *  different component subtree as in included template (included by an UIXInclude component) 
527    *  via. its facet. In this case the logical id of the move component will be in context of base  
528    *  document (as if it was never relocated) and not the document that defines the including 
529    *  component.
530    *  
531    * @see #getSourceScopedId()
532    */
533   public String getSourceLogicalScopedId()
534   {
535     return _moveCompAbsoluteLogicalScopedIdAtSource;
536   }
537   
538   /**
539    * Returns the absolute logical scopedId of the movable component as it would be after the move.
540    * 
541    * The id returned here will be in context of the document where the component is defined. For 
542    *  example, consider a component that is defined in a base document and is relocated to a 
543    *  different component subtree as in included template (included by an UIXInclude component) 
544    *  via. its facet. In this case the logical id of the move component will be in context of base  
545    *  document (as if it was never relocated) and not the document that defines the including 
546    *  component.
547    *  
548    * @see #getDestinationScopedId()
549    */
550   public String getDestinationLogicalScopedId()
551   {
552     return _moveCompAbsoluteLogicalScopedIdAtDestination;
553   }
554   
555   @Override
556   public boolean equals(Object o)
557   {
558     if (o == this)
559       return true;
560     
561     if (!(o instanceof MoveChildComponentChange))
562       return false;
563     
564     MoveChildComponentChange other = (MoveChildComponentChange)o;
565     
566     return  _equalsOrNull(_moveCompScopedIdAtSource, other._moveCompScopedIdAtSource) &&
567             _equalsOrNull(_moveCompAbsoluteScopedIdAtSource, 
568                           other._moveCompAbsoluteScopedIdAtSource) &&
569             _equalsOrNull(_moveCompAbsoluteLogicalScopedIdAtSource, 
570                           other._moveCompAbsoluteLogicalScopedIdAtSource) &&
571             _equalsOrNull(_moveCompDocPath, other._moveCompDocPath) &&
572             _equalsOrNull(_moveCompParentScopedId, other._moveCompParentScopedId) &&
573             _equalsOrNull(_moveCompAbsoluteScopedIdAtDestination, 
574                           other._moveCompAbsoluteScopedIdAtDestination) &&
575             _equalsOrNull(_moveCompAbsoluteLogicalScopedIdAtDestination, 
576                           other._moveCompAbsoluteLogicalScopedIdAtDestination) &&
577             _equalsOrNull(_destinationContainerScopedId, other._destinationContainerScopedId) &&
578             _equalsOrNull(_destinationContainerDocPath, other._destinationContainerDocPath) &&
579             _equalsOrNull(_commonParentScopedId, other._commonParentScopedId) &&
580             _equalsOrNull(_commonParentDocPath, other._commonParentDocPath) &&
581             _equalsOrNull(_insertBeforeCompId, other._insertBeforeCompId);
582   }
583   
584   @Override
585   public int hashCode()
586   {
587     return ((_moveCompScopedIdAtSource == null) ? 0 : _moveCompScopedIdAtSource.hashCode()) + 
588             37 * ((_moveCompAbsoluteScopedIdAtSource == null) ? 
589                   0 : _moveCompAbsoluteScopedIdAtSource.hashCode()) +
590             37 * ((_moveCompAbsoluteLogicalScopedIdAtSource == null) ? 
591                   0 : _moveCompAbsoluteLogicalScopedIdAtSource.hashCode()) +
592             37 * ((_moveCompDocPath == null) ? 0 : _moveCompDocPath.hashCode()) +
593             37 * ((_moveCompParentScopedId == null) ? 
594                   0 : _moveCompParentScopedId.hashCode()) +
595             37 * ((_moveCompAbsoluteScopedIdAtDestination == null) ? 
596                   0 : _moveCompAbsoluteScopedIdAtDestination.hashCode()) +
597             37 * ((_moveCompAbsoluteLogicalScopedIdAtDestination == null) ? 
598                   0 : _moveCompAbsoluteLogicalScopedIdAtDestination.hashCode()) +
599             37 * ((_destinationContainerScopedId == null) ? 
600                   0 : _destinationContainerScopedId.hashCode()) +
601             37 * ((_destinationContainerDocPath == null) ? 
602                   0 : _destinationContainerDocPath.hashCode()) +
603             37 * ((_commonParentScopedId == null) ? 0 : _commonParentScopedId.hashCode()) +
604             37 * ((_commonParentDocPath == null) ? 0 : _commonParentDocPath.hashCode()) +
605             37 * ((_insertBeforeCompId == null) ? 0 : _insertBeforeCompId.hashCode());
606   }
607       
608   @Override
609   public String toString()
610   {
611     StringBuffer sb = new StringBuffer();
612     
613     sb.append(super.toString());
614     sb.append("[moveCompAbsoluteLogicalScopedIdAtSource=").
615        append(_moveCompAbsoluteScopedIdAtSource);
616     sb.append(" moveCompAbsoluteLogicalScopedIdAtDestination=").
617        append(_moveCompAbsoluteLogicalScopedIdAtDestination);
618     sb.append(" moveCompAbsoluteScopedIdAtSource=").append(_moveCompAbsoluteScopedIdAtSource);
619     sb.append(" moveCompAbsoluteScopedIdAtDestination=").
620        append(_moveCompAbsoluteScopedIdAtDestination);
621     sb.append(" insertBeforeCompId=").append(_insertBeforeCompId);
622     sb.append(" commonParentScopedId=").append(_commonParentScopedId);
623     sb.append(" moveCompDocPath=").append(_moveCompDocPath);
624     sb.append(" destinationContainerDocPath=").append(_destinationContainerDocPath);
625     
626     return sb.append("]").toString();
627   }
628 
629   /**
630    * Returns best common parent of the two supplied components in a subtree to which this move 
631    * change can be added.
632    * - If the supplied components belong to same document, we try to get a common parent that is of
633    *   type UIXComponent and belongs to the same document, this is to be able to support applying 
634    *   document change.
635    * - If they do not belong to same document, we just return the closest common UIComponent parent, 
636    *   this should suffice to apply component change.
637    * 
638    * @throws IllegalArgumentException if we are not able to find the common parent for the supplied
639    *          components
640    */
641   private UIComponent _getValidatedCommonParent(
642     FacesContext context,
643     UIComponent componentToMove,
644     UIComponent parentAtDestination) 
645   {
646     // Calculate the depth of each node.
647     int firstDepth = _computeDepth(componentToMove);
648     int secondDepth = _computeDepth(parentAtDestination);
649            
650     // Move the deeper of the two components to its ancestor at the same depth
651     // as the shallower.
652     if (secondDepth > firstDepth)
653     {
654       parentAtDestination = _getAncestor(parentAtDestination, secondDepth - firstDepth);
655     }
656     else if(secondDepth < firstDepth)
657     {
658       componentToMove = _getAncestor(componentToMove, firstDepth - secondDepth);
659     }
660 
661     // Crawl up until we find the shared ancestor.
662     while (componentToMove != null && (componentToMove != parentAtDestination))
663     {
664       componentToMove = componentToMove.getParent();
665       parentAtDestination = parentAtDestination.getParent();
666     }
667     
668     UIComponent commonParent = componentToMove;
669     
670     // try to find a better ancestor complying these two conditions
671     //  1. The common parent is a UIXComponent instance - only UIXComponents have tags
672     //  2. The common parent belongs to same document that the other two participating components 
673     //     belong to
674     if (_moveCompDocPath.equals(_destinationContainerDocPath))
675     {
676       commonParent = _getUIXComponentAncestorInDoc(context, commonParent);
677     }
678     
679     // we cannot proceed if we could not find the common parent
680     if (commonParent == null)
681     {
682       throw new IllegalArgumentException(_LOG.getMessage("COMMON_PARENT_NOT_FOUND"));
683     }
684     
685     return commonParent;
686   }
687   
688   /**
689    * For a supplied base component, returns the closest UIXComponent ancestor that is in same 
690    * document as base component. If no such component is found, returns the supplied base component.
691    */
692   private UIComponent _getUIXComponentAncestorInDoc(
693     FacesContext context, 
694     UIComponent baseComp)
695   {
696     UIComponent ancestor = baseComp;
697     
698     while (ancestor != null)
699     {
700       if (ancestor instanceof UIXComponent &&
701           _moveCompDocPath.equals(ComponentUtils.getDocumentLocationForComponent(context, 
702                                                                                  ancestor)))
703       {
704         return ancestor;
705       }
706 
707       ancestor = ancestor.getParent();
708     }
709     
710     return baseComp;
711   }
712   
713   /**
714    * Trims the supplied scoped id of the supplied component until its immediate naming container
715    *  and returns.
716    */
717   private String _getScopedIdPrefix(UIComponent component, String scopedId)
718   {
719     if (component instanceof NamingContainer)
720       return scopedId;
721     else
722     {
723       // remove the component's id from the end
724       int separatorIndex = scopedId.lastIndexOf(NamingContainer.SEPARATOR_CHAR);
725       
726       if (separatorIndex >= 0)
727         return scopedId.substring(0, separatorIndex);
728       else
729       {
730         // component was at top level
731         return null;
732       }
733     }
734   }
735   
736   private boolean _equalsOrNull(Object obj1, Object obj2)
737   {
738     return (obj1 == null) ? (obj2 == null) : obj1.equals(obj2);
739   }
740 
741   /**
742    * Returns the destination container if passed non-null after doing needed validations. If null 
743    * destinationContainer is passed, determines it from the supplied insertBeforeComponent. 
744    */
745   private static UIComponent _getValidatedDestinationContainer(
746     UIComponent destinationContainer, 
747     UIComponent insertBeforeComponent)
748   {
749     if (insertBeforeComponent != null)
750     {
751       UIComponent parent = insertBeforeComponent.getParent();
752       
753       if (destinationContainer == null)
754       {
755         destinationContainer = parent;
756       }
757       
758       // if container was supplied, it better be parent of component to move next to
759       else if (destinationContainer != parent)
760       {
761         throw new IllegalArgumentException(
762           _LOG.getMessage("DESTINATION_CONTAINER_NOT_INSERTBEFORES_PARENT"));
763       }
764     }
765     else if (destinationContainer == null)
766     {
767       throw new IllegalArgumentException(_LOG.getMessage("DESTINATION_CONTAINER_REQUIRED"));
768     }
769     
770     return destinationContainer;
771   }
772   
773   /**
774    * Returns the depth of a UIComponent in the tree. 
775    * @param comp the UIComponent whose depth has to be calculated
776    * @return the depth of the passed in UIComponent
777    */
778   private static int _computeDepth(UIComponent comp) 
779   {
780     int i = 0;
781     while((comp = comp.getParent()) != null) 
782     {
783       i++;
784     }
785     return i;
786   }
787 
788   /**
789    * Returns the nth ancestor of the passed in component.
790    * @param component The UIComponent whose nth ancestor has to be found
791    * @param level Indicates how many levels to go up from the component
792    * @return The nth ancestor of the component
793    */
794   private static UIComponent _getAncestor(UIComponent component, int level) 
795   {
796     assert(level >= 0);
797     
798     while(level > 0)
799     {
800       component = component.getParent();
801       level--;
802     }
803     return component;
804   }
805   
806   // *ScopedId -> the id of the component scoped / relative to the common parent
807   // *AbsoluteScopedId -> the id of the component scoped / relative to the ViewRoot
808   // *AbsoluteLogicalScopedId -> id of the component scoped / relative to the ViewRoot in context
809   //    of the document in which it is originally defined. For more details, see documentation in 
810   //    getSourceLogicalScopedId() 
811   
812   // for component to be moved
813   private final String _moveCompScopedIdAtSource;
814   private final String _moveCompAbsoluteScopedIdAtSource;
815   private final String _moveCompAbsoluteLogicalScopedIdAtSource;
816   private final String _moveCompDocPath;
817   private final String _moveCompParentScopedId;
818 
819   // for component at destination after the move
820   private final String _moveCompAbsoluteScopedIdAtDestination;
821   private final String _moveCompAbsoluteLogicalScopedIdAtDestination;
822   
823   // for new parent at destination
824   private final String _destinationContainerScopedId;
825   private final String _destinationContainerDocPath;
826 
827   // for parent common to the source and new parent at destination
828   private final String _commonParentScopedId;
829   private final String _commonParentDocPath;
830 
831   // for immediate sibling of moved component at destination
832   private final String _insertBeforeCompId;
833 
834   private static final long serialVersionUID = 1L;
835 
836   private static final TrinidadLogger _LOG = 
837     TrinidadLogger.createTrinidadLogger(MoveChildComponentChange.class);
838 }