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 identifier as the movable child were to be already present in the
42   * destination container, it will be considered as a duplicate child, will be
43   * removed and movable child will be added.
44   * @see #add(FacesContext, ChangeManager)
45   * @see ChangeManager#addComponentChange(FacesContext, UIComponent, ComponentChange)
46   * @see ChangeManager#addDocumentChange(FacesContext, UIComponent, DocumentChange)
47   */
48  public final class MoveChildComponentChange 
49    extends ComponentChange
50    implements DocumentChange
51  {
52    /**
53     * Constructs a MoveChildComponentChange. The child will be appended to the 
54     * list of children of the destinationContainer.
55     * @param movableChild The child component to be moved.
56     * @param destinationContainer The destination component into which the child 
57     * component is to be moved.
58     * @throws IllegalArgumentException If movableChild or destinationContainer
59     * is null
60     */
61    public MoveChildComponentChange(
62      UIComponent movableChild,
63      UIComponent destinationContainer)
64    {
65      this(movableChild, destinationContainer, null);
66    }
67    
68    /**
69     * Constructs a MoveChildComponentChange. The child will be inserted to the 
70     * list of children of the destinationContainer, before the supplied 
71     * insertBeforecomponent. If the supplied insertBeforeComponent is null, the 
72     * child will be appended to the list of children of the destinationContainer.
73     * If the insertBeforeComponent is non-null, and if it were not to be found
74     * while applying this change, the movableChild will not be moved.
75     * @param movableChild The child component to be moved.
76     * @param destinationContainer The destination component into which the child 
77     * component is to be moved.
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 or destinationContainer
82     * is null, or if a parent component common to movableChild and 
83     * destinationContainer could not be found.
84     */
85    public MoveChildComponentChange(
86      UIComponent movableChild,
87      UIComponent destinationContainer, 
88      UIComponent insertBeforeComponent)
89    {
90      if (movableChild == null)
91        throw new IllegalArgumentException(
92          _LOG.getMessage("MOVABLE_CHILD_REQUIRED"));
93  
94      if (destinationContainer == null)
95        throw new IllegalArgumentException(
96          _LOG.getMessage("DESTINATION_CONTAINER_REQUIRED"));
97  
98      // Get the common parent
99      _commonParent = 
100       _getClosestCommonParentUIXComponent(movableChild, destinationContainer);
101     
102     if (_commonParent == null)
103       throw new IllegalArgumentException(
104         _LOG.getMessage("COMMON_PARENT_NOT_FOUND"));
105     
106     // Get the scoped id's for all participants
107     _movableChildScopedId = 
108       ComponentUtils.getScopedIdForComponent(movableChild, _commonParent);
109     _sourceParentScopedId = 
110       ComponentUtils.getScopedIdForComponent(movableChild.getParent(), 
111                                             _commonParent);
112     _destinationContainerScopedId = 
113       ComponentUtils.getScopedIdForComponent(destinationContainer, _commonParent);
114           
115     _commonParentScopedId = 
116       ComponentUtils.getScopedIdForComponent(_commonParent, null);
117 
118     if (_movableChildScopedId == null || 
119         _sourceParentScopedId == null || 
120         _destinationContainerScopedId == null ||
121         _commonParentScopedId == null)
122       throw new IllegalArgumentException(
123         _LOG.getMessage("MOVE_PARTICIPANTS_WITHOUT_ID"));
124 
125     // calculate the absolute scoped ids for the source and destination so that we can
126     // handle remapping scoped ids in the SessionChangeManager    
127     String commonParentPrefix = _getScopedIdPrefix(_commonParent, _commonParentScopedId);
128       
129     _sourceAbsoluteScopedId = (commonParentPrefix != null)
130                                  ? new StringBuilder(commonParentPrefix).
131                                            append(NamingContainer.SEPARATOR_CHAR).
132                                            append(_movableChildScopedId).toString()
133                                  : _movableChildScopedId;
134     
135     // calculate the absolute scoped id of the destination
136     String destinationContainerPrefix = _getScopedIdPrefix(destinationContainer,
137                                                            _destinationContainerScopedId);
138     
139     StringBuilder destinationScopedIdBuilder = new StringBuilder();
140     
141     if (commonParentPrefix != null)
142     {
143       destinationScopedIdBuilder.append(commonParentPrefix).append(NamingContainer.SEPARATOR_CHAR);
144     }
145     
146     if (destinationContainerPrefix != null)
147     {
148       destinationScopedIdBuilder.append(destinationContainerPrefix).append(NamingContainer.SEPARATOR_CHAR);
149     }
150     
151     _destinationAbsoluteScopedId = destinationScopedIdBuilder.append(movableChild.getId()).toString();
152 
153     // For insertBeforeComponent, we do not care to obtain scoped id.
154     _insertBeforeId = (insertBeforeComponent == null) ? 
155       null:insertBeforeComponent.getId();
156   }
157   
158   private String _getScopedIdPrefix(UIComponent component, String scopedId)
159   {
160     if (component instanceof NamingContainer)
161       return scopedId;
162     else
163     {
164       // remove the component's id from the end
165       int separatorIndex = scopedId.lastIndexOf(NamingContainer.SEPARATOR_CHAR);
166       
167       if (separatorIndex >= 0)
168         return scopedId.substring(0, separatorIndex);
169       else
170       {
171         // component was at top level
172         return null;
173       }
174     }
175   }
176   
177   /**
178    * Convenience method to add this MoveChildComponentChange to the supplied
179    * ChangeManager. The change will be registered against a parent component
180    * that is common to the child being moved and the container component at
181    * destination.
182    * @param facesContext The FacesContext instance for the current request
183    * @param changeManager The ChangeManager instance on which this
184    * MoveChildComponentChange is to be added.
185    * @return The common parent component against which this 
186    * MoveChildComponentChange was registered.
187    */
188   public UIComponent add(
189     FacesContext facesContext, 
190     ChangeManager changeManager) 
191   {
192     UIComponent commonParent = _commonParent;
193 
194     if (commonParent == null)
195       commonParent = 
196         facesContext.getViewRoot().findComponent(_commonParentScopedId);
197     if (commonParent == null)
198     {
199       _LOG.warning("COMMON_PARENT_NOT_FOUND", _commonParentScopedId);
200       return null;
201     }
202     
203     // Register a move change against the common parent
204     changeManager.addComponentChange(facesContext, commonParent, this);
205     
206     // We dont need to keep the common parent anymore
207     _commonParent = null;
208     
209     return commonParent;
210   }
211    
212   /**
213    * Apply this change to the specified component.
214    * @param changeTargetComponent The component that is a common parent to the 
215    * movable child and the destination container.
216    * @throws IllegalArgumentException If the supplied changeTargetComponent
217    * is null.
218    */
219   @Override
220   public void changeComponent(UIComponent changeTargetComponent)
221   {
222     if (changeTargetComponent == null)
223       throw new IllegalArgumentException(
224         _LOG.getMessage("COMPONENT_REQUIRED"));
225     
226     // 1. Check for destination container component 
227     UIComponent destinationContainer = 
228       changeTargetComponent.findComponent(_destinationContainerScopedId);
229     if(destinationContainer == null)
230     {
231       _LOG.warning("DESTINATION_CONTAINER_NOT_FOUND", 
232                    _destinationContainerScopedId);
233       return;
234     }
235     
236     // 2. Find movableChild, gather any duplicates and remove them.
237     //  Duplicates are possible because 
238     //  a) taghandlers re-create the component that was in the jspx file in
239     //    their original location, no matter whether it was moved/removed due to 
240     //    aplication of a different ComponentChange. Such components could now 
241     //    be considered duplicates. In theory, there could be just one such 
242     //    duplicate.
243     //  b) We would have moved/added components due to an earlier application of 
244     //    a ComponentChange, that could still be in the view tree. Such 
245     //    components must now be considered duplicates. In theory, there could 
246     //    be just one such duplicate.
247     // This issue of duplicates is more common when the movement is within same 
248     // NamingContainer.
249     UIComponent sourceParent = 
250       changeTargetComponent.findComponent(_sourceParentScopedId);
251     
252     UIComponent foundChild = 
253       changeTargetComponent.findComponent(_movableChildScopedId);
254     //Assume the first found child is the movableChild
255     UIComponent movableChild = foundChild;
256     int movableChildIndex = 0;
257     while (foundChild != null)
258     {
259       // If the parent matches, this is the one to move, rest are duplicates
260       if (foundChild.getParent().equals(sourceParent))
261       {
262         movableChild = foundChild;
263         movableChildIndex = sourceParent.getChildren().indexOf(movableChild);
264       }
265 
266       // Invariably, remove the found component from the tree. We remove the
267       //  movableChild also, otherwise, findComponent blind loops on this same 
268       //  component if movableChild and duplicates are within same immediate
269       //  NamingContainer.
270       foundChild.getParent().getChildren().remove(foundChild);
271 
272       // Try and find the next potential duplicate
273       foundChild = changeTargetComponent.findComponent(_movableChildScopedId);
274     }
275     
276     if(movableChild == null)
277     {
278       _LOG.warning("MOVABLE_CHILD_NOT_FOUND", _movableChildScopedId);
279       return;
280     }
281     
282     // Reattach the moveable child, so that move happens atomically at the end.
283     sourceParent.getChildren().add(movableChildIndex, movableChild);
284     
285     // 3. If there is a child already existing with the same identifier in the 
286     //  destination container, remove it. We are doing this before identifying 
287     //  the insert index so that insert index is accurate, in case we end up 
288     //  removing any child in this step.
289     String movableChildId = movableChild.getId();
290     int indexOfChildWithSameIdAtDestination = 0;
291     UIComponent childWithSameIdAtDestination = null;
292     for (UIComponent childComponent:destinationContainer.getChildren())
293     {
294       if (movableChildId.equals(childComponent.getId()))
295       {
296         indexOfChildWithSameIdAtDestination = 
297           destinationContainer.getChildren().indexOf(childComponent);
298         childWithSameIdAtDestination = childComponent;
299         destinationContainer.getChildren().remove(childComponent);
300       }
301     }
302     
303     // 4. See if we can find the insertBeforeComponent among the 
304     //  destinationContainer's children
305     int insertIndex = -1;
306     if (_insertBeforeId != null)
307     {
308       for (UIComponent childComponent:destinationContainer.getChildren())
309       {
310         if (_insertBeforeId.equals(childComponent.getId()))
311         {
312           insertIndex = 
313             destinationContainer.getChildren().indexOf(childComponent);
314           break;
315         }
316       }
317   
318       // insertBeforeId was specified, but corresponding component is missing. 
319       //  In this case abort the move, after re-adding any components in 
320       //  destination container that we may have deleted from step #3.
321       if (insertIndex == -1)
322       {
323         if (childWithSameIdAtDestination != null)
324           destinationContainer.getChildren().add(
325             indexOfChildWithSameIdAtDestination,
326             childWithSameIdAtDestination);
327         
328         _LOG.warning("INSERT_BEFORE_NOT_FOUND", _insertBeforeId);
329         return;
330       }
331     }
332     
333     // 5. Atomically move the child
334     if (insertIndex == -1)
335       destinationContainer.getChildren().add(movableChild);
336     else
337       destinationContainer.getChildren().add(insertIndex, movableChild);
338   }
339   
340   /**
341    * Given the DOM Node representing a Component, apply any necessary
342    * DOM changes. The node passed will be the Node that is a common parent for
343    * the movable child and the destination container.
344    * There is a limitation with the document change, that the movable child 
345    * Node, destination container Node, and the common parent Node have to belong
346    * to the same document.
347    * @param changeTargetNode DOM Node that is a common parent for the movable
348    * child and the destination container.
349    * @throws IllegalArgumentException If changeTargeNode were to be null.
350    */
351   public void changeDocument(Node changeTargetNode)
352   {
353     if (changeTargetNode == null)
354       throw new IllegalArgumentException(_LOG.getMessage("NO_NODE_SPECIFIED"));
355 
356     // Move involves four steps.
357     // 1. Finding the child node, the source of move
358     Node movableChildNode = 
359       ChangeUtils.__findNodeByScopedId(changeTargetNode, 
360                                        _movableChildScopedId, 
361                                        Integer.MAX_VALUE);
362     
363     if(movableChildNode == null)
364     {
365       _LOG.warning("MOVABLE_CHILD_NOT_FOUND", _movableChildScopedId);
366       return;
367     }
368     
369     // 2. Finding the destination container node
370     Node destinationContainerNode = 
371       ChangeUtils.__findNodeByScopedId(changeTargetNode, 
372                                        _destinationContainerScopedId, 
373                                        Integer.MAX_VALUE);
374 
375     
376     if(destinationContainerNode == null)
377     {
378       _LOG.warning("DESTINATION_CONTAINER_NOT_FOUND", 
379                    _destinationContainerScopedId);
380       return;
381     }
382     
383     //3. Finding the neighbor at the destination
384     Node insertBeforeNode = (_insertBeforeId == null) ? 
385       null:ChangeUtils.__findNodeByScopedId(destinationContainerNode, 
386                                             _insertBeforeId, 
387                                             1);
388     // insertBeforeId was specified, but corresponding component is missing.
389     //  Abort the move.
390     if(_insertBeforeId != null && insertBeforeNode == null)
391     {
392       _LOG.warning("INSERT_BEFORE_NOT_FOUND", _insertBeforeId);
393       return;
394     }
395 
396     //4. Atomically move the child.
397     destinationContainerNode.insertBefore(movableChildNode, insertBeforeNode);
398   }
399 
400   /** 
401    * Returns true if adding the DocumentChange should force the JSP Document
402    * to reload
403    * @return true Since moving of components should force the document to reload
404    */
405   public boolean getForcesDocumentReload()
406   {
407     return true;
408   }
409   
410   /**
411    * Returns the first UIXComponent common parent of two components in a
412    * subtree.
413    * @param firstComponent The first UIComponent instance
414    * @param secondComponent The second UIComponent instance
415    * @return UIComponent The closest common parent of the two supplied 
416    * components.
417    */
418   private static UIComponent _getClosestCommonParentUIXComponent(
419     UIComponent firstComponent,
420     UIComponent secondComponent) 
421   {
422     if (firstComponent == null || secondComponent == null)
423       return null;
424 
425     // Calculate the depth of each node.
426     int firstDepth = _computeDepth(firstComponent);
427     int secondDepth = _computeDepth(secondComponent);
428            
429     // Move the deeper of the two components to its ancestor at the same depth
430     // as the shallower.
431     if (secondDepth > firstDepth)
432     {
433       secondComponent = _getAncestor(secondComponent, secondDepth - firstDepth);
434     }
435     else if(secondDepth < firstDepth)
436     {
437       firstComponent = _getAncestor(firstComponent, firstDepth - secondDepth);
438     }
439 
440     // Crawl up until we find the shared ancestor.
441     while (firstComponent != null && (firstComponent != secondComponent))
442     {
443       firstComponent = firstComponent.getParent();
444       secondComponent = secondComponent.getParent();
445     }
446 
447     // Crawl up to first UIXComponent shared parent, since only UIXComponents 
448     // have tags that apply changes.
449     UIComponent sharedRoot = firstComponent;
450 
451     while ((sharedRoot != null) && !(sharedRoot instanceof UIXComponent))
452       sharedRoot = sharedRoot.getParent();
453           
454     return sharedRoot;
455   }
456   
457   /**
458    * Returns the absolute scopedId of the source component
459    */
460   public String getSourceScopedId()
461   {
462     return _sourceAbsoluteScopedId;
463   }
464 
465     
466   /**
467    * Returns the absolute scopedId of the source component at its destination
468    */
469   public String getDestinationScopedId()
470   {
471     return _destinationAbsoluteScopedId;
472   }
473   
474   /**
475    * Returns the depth of a UIComponent in the tree. 
476    * @param comp the UIComponent whose depth has to be calculated
477    * @return the depth of the passed in UIComponent
478    */
479   private static int _computeDepth(UIComponent comp) 
480   {
481     int i = 0;
482     while((comp = comp.getParent()) != null) 
483     {
484       i++;
485     }
486     return i;
487   }
488 
489   /**
490    * Returns the nth ancestor of the passed in component.
491    * @param component The UIComponent whose nth ancestor has to be found
492    * @param level Indicates how many levels to go up from the component
493    * @return The nth ancestor of the component
494    */
495   private static UIComponent _getAncestor(UIComponent component, int level) 
496   {
497     assert(level >= 0);
498     
499     while(level > 0)
500     {
501       component = component.getParent();
502       level--;
503     }
504     return component;
505   }
506   
507   private transient UIComponent _commonParent;
508 
509   private final String _movableChildScopedId;
510   private final String _sourceParentScopedId;
511   private final String _destinationContainerScopedId;
512   private final String _commonParentScopedId;
513   private final String _insertBeforeId;
514   private final String _sourceAbsoluteScopedId;
515   private final String _destinationAbsoluteScopedId;
516   private static final long serialVersionUID = 1L;
517 
518   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(
519     MoveChildComponentChange.class);
520 }