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.custom.tree2;
20  
21  import java.io.IOException;
22  import java.io.Serializable;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  import javax.faces.FacesException;
30  import javax.faces.application.FacesMessage;
31  import javax.faces.component.ContextCallback;
32  import javax.faces.component.EditableValueHolder;
33  import javax.faces.component.NamingContainer;
34  import javax.faces.component.UIComponent;
35  import javax.faces.component.UIComponentBase;
36  import javax.faces.component.UINamingContainer;
37  import javax.faces.component.UIViewRoot;
38  import javax.faces.component.UniqueIdVendor;
39  import javax.faces.component.visit.VisitCallback;
40  import javax.faces.component.visit.VisitContext;
41  import javax.faces.component.visit.VisitResult;
42  import javax.faces.context.ExternalContext;
43  import javax.faces.context.FacesContext;
44  import javax.faces.el.ValueBinding;
45  import javax.faces.event.AbortProcessingException;
46  import javax.faces.event.ActionEvent;
47  import javax.faces.event.FacesEvent;
48  import javax.faces.event.FacesListener;
49  import javax.faces.event.PhaseId;
50  
51  import org.apache.commons.logging.Log;
52  import org.apache.commons.logging.LogFactory;
53  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFComponent;
54  import org.apache.myfaces.shared_tomahawk.util.MessageUtils;
55  import org.apache.myfaces.tomahawk.util.Constants;
56  
57  /**
58   * TreeData is a {@link UIComponent} that supports binding data stored in a tree represented
59   * by a {@link TreeNode} instance.  During iterative processing over the tree nodes in the
60   * data model, the object for the current node is exposed as a request attribute under the key
61   * specified by the <code>var</code> property.  {@link javax.faces.render.Renderer}s of this
62   * component should use the appropriate facet to assist in rendering.
63   *
64   * @author Sean Schofield
65   * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
66   * @version $Revision: 703742 $ $Date: 2008-10-11 17:10:36 -0500 (sáb, 11 oct 2008) $
67   */
68  @JSFComponent
69  public class UITreeData extends UIComponentBase implements NamingContainer, Tree, UniqueIdVendor {
70      private Log log = LogFactory.getLog(UITreeData.class);
71  
72      public static final String COMPONENT_TYPE = "org.apache.myfaces.UITree2";
73      public static final String COMPONENT_FAMILY = "org.apache.myfaces.HtmlTree2";
74      //private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.Tree2";
75      private static final String MISSING_NODE = "org.apache.myfaces.tree2.MISSING_NODE";
76      private static final int PROCESS_DECODES = 1;
77      private static final int PROCESS_VALIDATORS = 2;
78      private static final int PROCESS_UPDATES = 3;
79  
80      private TreeModel _cachedModel;
81      private String _nodeId;
82      private TreeNode _node;
83  
84      private Object _value;
85      private String _var;
86      private Map _saved = new HashMap();
87  
88      private TreeState _restoredState = null;
89  
90      private transient FacesContext _facesContext;
91      
92      private static final String SKIP_ITERATION_HINT = "javax.faces.visit.SKIP_ITERATION";
93  
94      /**
95       * Constructor
96       */
97      public UITreeData()
98      {
99          //setRendererType(DEFAULT_RENDERER_TYPE);
100     }
101 
102 
103     // see superclass for documentation
104     public String getFamily()
105     {
106         return COMPONENT_FAMILY;
107     }
108 
109     //  see superclass for documentation
110     public Object saveState(FacesContext context)
111     {
112         Object values[] = new Object[3];
113         values[0] = super.saveState(context);
114         values[1] = _var;
115         values[2] = _restoredState;
116         return ((Object) (values));
117     }
118 
119 
120     // see superclass for documentation
121     public void restoreState(FacesContext context, Object state)
122     {
123         Object values[] = (Object[]) state;
124         super.restoreState(context, values[0]);
125 
126         _var = (String)values[1];
127         _restoredState = (TreeState) values[2];
128     }
129 
130     public void encodeEnd(FacesContext context) throws IOException {
131         super.encodeEnd(context);
132 
133         // prepare to save the tree state -- fix for MYFACES-618
134         // should be done in saveState() but Sun RI does not call saveState() and restoreState()
135         // with javax.faces.STATE_SAVING_METHOD = server
136         TreeState state = getDataModel().getTreeState();
137         if ( state == null)
138         {
139             // the model supplier has forgotten to return a valid state manager, but we need one
140             state = new TreeStateBase();
141         }
142         // save the state with the component, unless it should explicitly not saved eg. session-scoped model and state
143         _restoredState = (state.isTransient()) ? null : state;
144 
145     }
146 
147     public void queueEvent(FacesEvent event)
148     {
149         super.queueEvent(new FacesEventWrapper(event, getNodeId(), this));
150     }
151 
152 
153     public void broadcast(FacesEvent event) throws AbortProcessingException
154     {
155         if (event instanceof FacesEventWrapper)
156         {
157             FacesEventWrapper childEvent = (FacesEventWrapper) event;
158             String currNodeId = getNodeId();
159             setNodeId(childEvent.getNodeId());
160             FacesEvent nodeEvent = childEvent.getFacesEvent();
161             nodeEvent.getComponent().broadcast(nodeEvent);
162             setNodeId(currNodeId);
163             return;
164         }
165         else if(event instanceof ToggleExpandedEvent)
166         {
167             ToggleExpandedEvent toggleEvent = (ToggleExpandedEvent) event;
168             String currentNodeId = getNodeId();
169             setNodeId(toggleEvent.getNodeId());
170             toggleExpanded();
171             setNodeId(currentNodeId);
172         }
173         else
174         {
175             super.broadcast(event);
176             return;
177         }
178     }
179 
180 
181     // see superclass for documentation
182     public void processDecodes(FacesContext context)
183     {
184         if (context == null) throw new NullPointerException("context");
185         if (!isRendered()) return;
186 
187         _cachedModel = null;
188         _saved = new HashMap();
189 
190         setNodeId(null);
191         decode(context);
192 
193         processNodes(context, PROCESS_DECODES, getDataModel().getTreeWalker());
194         // After processNodes is executed, the node active is the last one
195         // we have to set it to null again to avoid inconsistency on outsider
196         // code (just like UIData components does)
197         setNodeId(null);
198 
199     }
200 
201     // see superclass for documentation
202     public void processValidators(FacesContext context)
203     {
204         if (context == null) throw new NullPointerException("context");
205         if (!isRendered()) return;
206 
207         processNodes(context, PROCESS_VALIDATORS, getDataModel().getTreeWalker());
208 
209         setNodeId(null);
210     }
211 
212 
213     // see superclass for documentation
214     public void processUpdates(FacesContext context)
215     {
216         if (context == null) throw new NullPointerException("context");
217         if (!isRendered()) return;
218 
219         processNodes(context, PROCESS_UPDATES, getDataModel().getTreeWalker());
220 
221         setNodeId(null);
222     }
223 
224     // see superclass for documentation
225     /*
226     public String getClientId(FacesContext context)
227     {
228         String ownClientId = super.getClientId(context);
229         if (_nodeId != null)
230         {
231             return ownClientId + NamingContainer.SEPARATOR_CHAR + _nodeId;
232         } else
233         {
234             return ownClientId;
235         }
236     }*/
237     
238     @Override
239     public String getContainerClientId(FacesContext context)
240     {
241         String ownClientId = super.getContainerClientId(context);
242         if (_nodeId != null)
243         {
244             return ownClientId + UINamingContainer.getSeparatorChar(context) + _nodeId;
245         } 
246         else
247         {
248             return ownClientId;
249         }
250     }
251 
252     // see superclass for documentation
253     public void setValueBinding(String name, ValueBinding binding)
254     {
255         if ("value".equals(name))
256         {
257             _cachedModel = null;
258         } else if ("nodeVar".equals(name) || "nodeId".equals(name) || "treeVar".equals(name))
259         {
260             throw new IllegalArgumentException("name " + name);
261         }
262         super.setValueBinding(name, binding);
263     }
264 
265     // see superclass for documentation
266     public void encodeBegin(FacesContext context) throws IOException
267     {
268         /**
269          * The renderer will handle most of the encoding, but if there are any
270          * error messages queued for the components (validation errors), we
271          * do want to keep the saved state so that we can render the node with
272          * the invalid value.
273          */
274 
275         if (!keepSaved(context))
276         {
277             _saved = new HashMap();
278         }
279 
280         // FIX for MYFACES-404
281         // do not use the cached model the render phase
282         _cachedModel = null;
283 
284         super.encodeBegin(context);
285     }
286 
287     /**
288      * Sets the value of the TreeData.
289      *
290      * @param value The new value
291      *
292      * @deprecated
293      */
294     public void setValue(Object value)
295     {
296         _cachedModel = null;
297         _value = value;
298     }
299 
300 
301     /**
302      * Gets the model of the TreeData -
303      *  due to backwards-compatibility, this can also be retrieved by getValue.
304      *
305      * @return The value
306      */
307     public Object getModel()
308     {
309         return getValue();
310     }
311 
312     /**
313      * Sets the model of the TreeData -
314      *  due to backwards-compatibility, this can also be set by calling setValue.
315      *
316      * @param model The new model
317      */
318     public void setModel(Object model)
319     {
320         setValue(model);
321     }
322 
323 
324     /**
325      * Gets the value of the TreeData.
326      *
327      * @JSFProperty
328      *   required="true"
329      * @return The value
330      *
331      * @deprecated
332      */
333     public Object getValue()
334     {
335         if (_value != null) return _value;
336         ValueBinding vb = getValueBinding("value");
337         return vb != null ? vb.getValue(getFacesContext()) : null;
338     }
339 
340     /**
341      * Set the request-scope attribute under which the data object for the current node wil be exposed
342      * when iterating.
343      *
344      * @param var The new request-scope attribute name
345      */
346     public void setVar(String var)
347     {
348         _var = var;
349     }
350 
351 
352     /**
353      * Return the request-scope attribute under which the data object for the current node will be exposed
354      * when iterating. This property is not enabled for value binding expressions.
355      * 
356      * @JSFProperty
357      * @return The iterator attribute
358      */
359     public String getVar()
360     {
361         return _var;
362     }
363 
364     /**
365      * Calls through to the {@link TreeModel} and returns the current {@link TreeNode} or <code>null</code>.
366      *
367      * @return The current node
368      */
369     public TreeNode getNode()
370     {
371         return _node;
372     }
373 
374 
375     public String getNodeId()
376     {
377         return _nodeId;
378     }
379 
380 
381     public void setNodeId(String nodeId)
382     {
383         saveDescendantState();
384 
385         _nodeId = nodeId;
386 
387         TreeModel model = getDataModel();
388         if (model == null)
389         {
390             return;
391         }
392 
393         try
394         {
395             _node = model.getNodeById(nodeId);
396         }
397         //TODO: change to an own exception
398         catch (IndexOutOfBoundsException aob)
399         {
400             /**
401              * This might happen if we are trying to process a commandLink for a node that node that no longer
402              * exists.  Instead of allowing a RuntimeException to crash the application, we will add a warning
403              * message so the user can optionally display the warning.  Also, we will allow the user to provide
404              * their own value binding method to be called so they can handle it how they see fit.
405              */
406             FacesMessage message = MessageUtils.getMessageFromBundle(Constants.TOMAHAWK_DEFAULT_BUNDLE, MISSING_NODE, new String[] {nodeId});
407             message.setSeverity(FacesMessage.SEVERITY_WARN);
408             FacesContext.getCurrentInstance().addMessage(getId(), message);
409 
410             /** @todo call hook */
411             /** @todo figure out whether or not to abort this method gracefully */
412         }
413 
414         restoreDescendantState();
415 
416         if (_var != null)
417         {
418             Map requestMap = getFacesContext().getExternalContext().getRequestMap();
419 
420             if (nodeId == null)
421             {
422                 requestMap.remove(_var);
423             } else
424             {
425                 requestMap.put(_var, getNode());
426             }
427         }
428     }
429 
430     @Override
431     public boolean invokeOnComponent(FacesContext context, String clientId, ContextCallback callback)
432         throws FacesException
433     {
434         if (context == null || clientId == null || callback == null)
435         {
436             throw new NullPointerException();
437         }
438         
439         final String baseClientId = getClientId(context);
440 
441         // searching for this component?
442         boolean returnValue = baseClientId.equals(clientId);
443 
444         boolean isTemporalFacesContext = isTemporalFacesContext();
445         if (!isTemporalFacesContext)
446         {
447             setTemporalFacesContext(context);
448         }
449         
450         pushComponentToEL(context, this);
451         try
452         {
453             if (returnValue)
454             {
455                 try
456                 {
457                     callback.invokeContextCallback(context, this);
458                     return true;
459                 }
460                 catch (Exception e)
461                 {
462                     throw new FacesException(e);
463                 }
464             }
465     
466             // Now Look throught facets on this UIComponent
467             for (Iterator<UIComponent> it = this.getFacets().values().iterator(); !returnValue && it.hasNext();)
468             {
469                 returnValue = it.next().invokeOnComponent(context, clientId, callback);
470             }
471     
472             if (returnValue)
473             {
474                 return returnValue;
475             }
476             
477             // is the component an inner component?
478             if (clientId.startsWith(baseClientId))
479             {
480                 TreeModel model = getDataModel();
481                 
482                 //We only use the tree walker to get the RootNodeId()
483                 TreeWalker walker = model.getTreeWalker();
484                 UIComponent facet = null;
485                 walker.reset();
486                 walker.setTree(this);
487                 
488                 String oldNodeId = getNodeId();
489                 
490                 try
491                 {
492                     while(!returnValue && walker.next())
493                     {
494                         TreeNode node = getNode();
495                         facet = getFacet(node.getType());
496     
497                         if (facet == null)
498                         {
499                             log.warn("Unable to locate facet with the name: " + node.getType());
500                             continue;
501                         }
502                         
503                         returnValue = facet.invokeOnComponent(context, baseClientId, callback);
504                     }
505                 }
506                 finally
507                 {
508                     setNodeId(oldNodeId);
509                 }
510             }
511         }
512         finally
513         {
514             //all components must call popComponentFromEl after visiting is finished
515             popComponentFromEL(context);
516             if (!isTemporalFacesContext)
517             {
518                 setTemporalFacesContext(null);
519             }
520         }
521 
522         return returnValue;
523     }
524     
525     @Override
526     public boolean visitTree(VisitContext context, VisitCallback callback)
527     {
528         if (!isVisitable(context))
529         {
530             return false;
531         }
532 
533         boolean isTemporalFacesContext = isTemporalFacesContext();
534         if (!isTemporalFacesContext)
535         {
536             setTemporalFacesContext(context.getFacesContext());
537         }
538         // save the current row index
539         String oldNodeId = getNodeId();
540         // set row index to -1 to process the facets and to get the rowless clientId
541         setNodeId(null);
542         // push the Component to EL
543         pushComponentToEL(context.getFacesContext(), this);
544         try
545         {
546             VisitResult visitResult = context.invokeVisitCallback(this,
547                     callback);
548             switch (visitResult)
549             {
550             //we are done nothing has to be processed anymore
551             case COMPLETE:
552                 return true;
553 
554             case REJECT:
555                 return false;
556 
557                 //accept
558             default:
559                 // determine if we need to visit our children 
560                 Collection<String> subtreeIdsToVisit = context
561                         .getSubtreeIdsToVisit(this);
562                 boolean doVisitChildren = subtreeIdsToVisit != null
563                         && !subtreeIdsToVisit.isEmpty();
564                 if (doVisitChildren)
565                 {
566                     Boolean skipIterationHint = (Boolean) context.getFacesContext().getAttributes().get(SKIP_ITERATION_HINT);
567                     if (skipIterationHint != null && skipIterationHint.booleanValue())
568                     {
569                         // If SKIP_ITERATION is enabled, do not take into account rows.
570                         if (getChildCount() > 0) {
571                             for (UIComponent child : getChildren()) {
572                                 if (child.visitTree(context, callback)) {
573                                     return true;
574                                 }
575                             }
576                         }
577                     }
578                     else
579                     {
580                         TreeWalker walker = getDataModel().getTreeWalker();
581                         UIComponent facet = null;
582                         walker.reset();
583                         walker.setTree(this);
584     
585                         while(walker.next())
586                         {
587                             TreeNode node = getNode();
588                             facet = getFacet(node.getType());
589     
590                             if (facet == null)
591                             {
592                                 log.warn("Unable to locate facet with the name: " + node.getType());
593                                 continue;
594                                 //throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
595                             }
596     
597                             if (facet.visitTree(context, callback))
598                             {
599                                 return true;
600                             }
601                         }
602                     }
603                 }
604             }
605         }
606         finally
607         {
608             // pop the component from EL and restore the old row index
609             popComponentFromEL(context.getFacesContext());
610             setNodeId(oldNodeId);
611             if (!isTemporalFacesContext)
612             {
613                 setTemporalFacesContext(null);
614             }
615         }
616 
617         // Return false to allow the visiting to continue
618         return false;
619     }
620     
621     @Override
622     protected FacesContext getFacesContext()
623     {
624         if (_facesContext == null)
625         {
626             return super.getFacesContext();
627         }
628         else
629         {
630             return _facesContext;
631         }
632     }
633     
634     private boolean isTemporalFacesContext()
635     {
636         return _facesContext != null;
637     }
638     
639     private void setTemporalFacesContext(FacesContext facesContext)
640     {
641         _facesContext = facesContext;
642     }
643     
644     /**
645      * Gets an array of String containing the ID's of all of the {@link TreeNode}s in the path to
646      * the specified node.  The path information will be an array of <code>String</code> objects
647      * representing node ID's. The array will starting with the ID of the root node and end with
648      * the ID of the specified node.
649      *
650      * @param nodeId The id of the node for whom the path information is needed.
651      * @return String[]
652      */
653     public String[] getPathInformation(String nodeId)
654     {
655         return getDataModel().getPathInformation(nodeId);
656     }
657 
658     /**
659      * Indicates whether or not the specified {@link TreeNode} is the last child in the <code>List</code>
660      * of children.  If the node id provided corresponds to the root node, this returns <code>true</code>.
661      *
662      * @param nodeId The ID of the node to check
663      * @return boolean
664      */
665     public boolean isLastChild(String nodeId)
666     {
667         return getDataModel().isLastChild(nodeId);
668     }
669 
670     /**
671      * Returns a previously cached {@link TreeModel}, if any, or sets the cache variable to either the
672      * current value (if its a {@link TreeModel}) or to a new instance of {@link TreeModel} (if it's a
673      * {@link TreeNode}) with the provided value object as the root node.
674      *
675      * @return TreeModel
676      */
677     public TreeModel getDataModel()
678     {
679         if (_cachedModel != null)
680         {
681             return _cachedModel;
682         }
683 
684         Object value = getValue();
685         if (value != null)
686         {
687             if (value instanceof TreeModel)
688             {
689                 _cachedModel = (TreeModel) value;
690             }
691             else if (value instanceof TreeNode)
692             {
693                 _cachedModel = new TreeModelBase((TreeNode) value);
694             } else
695             {
696                 throw new IllegalArgumentException("Value must be a TreeModel or TreeNode");
697             }
698         }
699 
700         if (_restoredState != null)
701             _cachedModel.setTreeState(_restoredState); // set the restored state (if there is one) on the model
702 
703         return _cachedModel;
704     }
705 
706     /**
707      * Epands all nodes by default.
708      */
709     public void expandAll()
710     {
711         toggleAll(true);
712     }
713 
714     /**
715      * Collapse all nodes by default.
716      */
717     public void collapseAll()
718     {
719         toggleAll(false);
720     }
721 
722     /**
723      * Toggles all of the nodes to either expanded or collapsed depending on the
724      * parameter supplied.
725      *
726      * @param expanded Expand all of the nodes (a value of false indicates collapse
727      * all nodes)
728      */
729     private void toggleAll(boolean expanded)
730     {
731         TreeWalker walker = getDataModel().getTreeWalker();
732         walker.reset();
733 
734         TreeState state =  getDataModel().getTreeState();
735         walker.setCheckState(false);
736         walker.setTree(this);
737 
738         while(walker.next())
739         {
740             String id = getNodeId();
741             if ((expanded && !state.isNodeExpanded(id)) || (!expanded && state.isNodeExpanded(id)))
742             {
743                 state.toggleExpanded(id);
744             }
745         }
746     }
747 
748     /**
749      * Expands all of the nodes in the specfied path.
750      * @param nodePath The path to expand.
751      */
752     public void expandPath(String[] nodePath)
753     {
754         getDataModel().getTreeState().expandPath(nodePath);
755     }
756 
757     /**
758      * Expands all of the nodes in the specfied path.
759      * @param nodePath The path to expand.
760      */
761     public void collapsePath(String[] nodePath)
762     {
763         getDataModel().getTreeState().collapsePath(nodePath);
764     }
765 
766 
767     protected void processNodes(FacesContext context, int processAction, TreeWalker walker)
768     {
769         UIComponent facet = null;
770         walker.reset();
771         walker.setTree(this);
772 
773         while(walker.next())
774         {
775             TreeNode node = getNode();
776             facet = getFacet(node.getType());
777 
778             if (facet == null)
779             {
780                 log.warn("Unable to locate facet with the name: " + node.getType());
781                 continue;
782                 //throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
783             }
784 
785             switch (processAction)
786             {
787                 case PROCESS_DECODES:
788 
789                     facet.processDecodes(context);
790                     break;
791 
792                 case PROCESS_VALIDATORS:
793 
794                     facet.processValidators(context);
795                     break;
796 
797                 case PROCESS_UPDATES:
798 
799                     facet.processUpdates(context);
800                     break;
801             }
802         }
803 
804     }
805 
806     /**
807      * To support using input components for the nodes (e.g., input fields, checkboxes, and selection
808      * lists) while still only using one set of components for all nodes, the state held by the components
809      * for the current node must be saved for a new node is selected.
810      */
811     private void saveDescendantState()
812     {
813         FacesContext context = getFacesContext();
814         Iterator i = getFacets().values().iterator();
815         while (i.hasNext())
816         {
817             UIComponent facet = (UIComponent) i.next();
818             saveDescendantState(facet, context);
819         }
820     }
821 
822     /**
823      * Overloaded helper method for the no argument version of this method.
824      *
825      * @param component The component whose state needs to be saved
826      * @param context   FacesContext
827      */
828     private void saveDescendantState(UIComponent component, FacesContext context)
829     {
830         if (component instanceof EditableValueHolder)
831         {
832             EditableValueHolder input = (EditableValueHolder) component;
833             String clientId = component.getClientId(context);
834             SavedState state = (SavedState) _saved.get(clientId);
835             if (state == null)
836             {
837                 state = new SavedState();
838                 _saved.put(clientId, state);
839             }
840             state.setValue(input.getLocalValue());
841             state.setValid(input.isValid());
842             state.setSubmittedValue(input.getSubmittedValue());
843             state.setLocalValueSet(input.isLocalValueSet());
844         }
845 
846         List kids = component.getChildren();
847         for (int i = 0; i < kids.size(); i++)
848         {
849             saveDescendantState((UIComponent) kids.get(i), context);
850         }
851     }
852 
853 
854     /**
855      * Used to configure a new node with the state stored previously.
856      */
857     private void restoreDescendantState()
858     {
859         FacesContext context = getFacesContext();
860         Iterator i = getFacets().values().iterator();
861         while (i.hasNext())
862         {
863             UIComponent facet = (UIComponent) i.next();
864             restoreDescendantState(facet, context);
865         }
866     }
867 
868     /**
869      * Overloaded helper method for the no argument version of this method.
870      *
871      * @param component The component whose state needs to be restored
872      * @param context   FacesContext
873      */
874     private void restoreDescendantState(UIComponent component, FacesContext context)
875     {
876         String id = component.getId();
877         component.setId(id); // forces the cilent id to be reset
878 
879         if (component instanceof EditableValueHolder)
880         {
881             EditableValueHolder input = (EditableValueHolder) component;
882             String clientId = component.getClientId(context);
883             SavedState state = (SavedState) _saved.get(clientId);
884             if (state == null)
885             {
886                 state = new SavedState();
887             }
888             input.setValue(state.getValue());
889             input.setValid(state.isValid());
890             input.setSubmittedValue(state.getSubmittedValue());
891             input.setLocalValueSet(state.isLocalValueSet());
892         }
893 
894         List kids = component.getChildren();
895         for (int i = 0; i < kids.size(); i++)
896         {
897             restoreDescendantState((UIComponent)kids.get(i), context);
898         }
899         Map facets = component.getFacets();
900         for(Iterator i = facets.values().iterator(); i.hasNext();)
901         {
902             restoreDescendantState((UIComponent)i.next(), context);
903         }
904     }
905 
906     /**
907      * A regular bean with accessor methods for all state variables.
908      *
909      * @author Sean Schofield
910      * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
911      * @version $Revision: 703742 $ $Date: 2008-10-11 17:10:36 -0500 (sáb, 11 oct 2008) $
912      */
913     private static class SavedState implements Serializable
914     {
915         private static final long serialVersionUID = 273343276957070557L;
916         private Object submittedValue;
917         private boolean valid = true;
918         private Object value;
919         private boolean localValueSet;
920 
921         Object getSubmittedValue()
922         {
923             return submittedValue;
924         }
925 
926         void setSubmittedValue(Object submittedValue)
927         {
928             this.submittedValue = submittedValue;
929         }
930 
931         boolean isValid()
932         {
933             return valid;
934         }
935 
936         void setValid(boolean valid)
937         {
938             this.valid = valid;
939         }
940 
941         Object getValue()
942         {
943             return value;
944         }
945 
946         void setValue(Object value)
947         {
948             this.value = value;
949         }
950 
951         boolean isLocalValueSet()
952         {
953             return localValueSet;
954         }
955 
956         void setLocalValueSet(boolean localValueSet)
957         {
958             this.localValueSet = localValueSet;
959         }
960     }
961 
962     /**
963      * Inner class used to wrap the original events produced by child components in the tree.
964      * This will allow the tree to find the appropriate component later when its time to
965      * broadcast the events to registered listeners.  Code is based on a similar private
966      * class for UIData.
967      */
968     private static class FacesEventWrapper extends FacesEvent
969     {
970         private static final long serialVersionUID = -3056153249469828447L;
971         private FacesEvent _wrappedFacesEvent;
972         private String _nodeId;
973 
974 
975         public FacesEventWrapper(FacesEvent facesEvent, String nodeId, UIComponent component)
976         {
977             super(component);
978             _wrappedFacesEvent = facesEvent;
979             _nodeId = nodeId;
980         }
981 
982 
983         public PhaseId getPhaseId()
984         {
985             return _wrappedFacesEvent.getPhaseId();
986         }
987 
988 
989         public void setPhaseId(PhaseId phaseId)
990         {
991             _wrappedFacesEvent.setPhaseId(phaseId);
992         }
993 
994 
995         public void queue()
996         {
997             _wrappedFacesEvent.queue();
998         }
999 
1000 
1001         public String toString()
1002         {
1003             return _wrappedFacesEvent.toString();
1004         }
1005 
1006 
1007         public boolean isAppropriateListener(FacesListener faceslistener)
1008         {
1009             // this event type is only intended for wrapping a real event
1010             return false;
1011         }
1012 
1013 
1014         public void processListener(FacesListener faceslistener)
1015         {
1016             throw new UnsupportedOperationException("This event type is only intended for wrapping a real event");
1017         }
1018 
1019 
1020         public FacesEvent getFacesEvent()
1021         {
1022             return _wrappedFacesEvent;
1023         }
1024 
1025 
1026         public String getNodeId()
1027         {
1028             return _nodeId;
1029         }
1030     }
1031 
1032     /**
1033      * Returns true if there is an error message queued for at least one of the nodes.
1034      *
1035      * @param context FacesContext
1036      * @return whether an error message is present
1037      */
1038     private boolean keepSaved(FacesContext context)
1039     {
1040         Iterator clientIds = _saved.keySet().iterator();
1041         while (clientIds.hasNext())
1042         {
1043             String clientId = (String) clientIds.next();
1044             Iterator messages = context.getMessages(clientId);
1045             while (messages.hasNext())
1046             {
1047                 FacesMessage message = (FacesMessage) messages.next();
1048                 if (message.getSeverity().compareTo(FacesMessage.SEVERITY_ERROR) >= 0)
1049                 {
1050                     return true;
1051                 }
1052             }
1053         }
1054 
1055         return false;
1056     }
1057 
1058     /**
1059      * Toggle the expanded state of the current node.
1060      */
1061     public void toggleExpanded()
1062     {
1063         getDataModel().getTreeState().toggleExpanded(getNodeId());
1064     }
1065 
1066     /**
1067      * Indicates whether or not the current {@link TreeNode} is expanded.
1068      * @return boolean
1069      */
1070     public boolean isNodeExpanded()
1071     {
1072         return getDataModel().getTreeState().isNodeExpanded(getNodeId());
1073     }
1074 
1075     /**
1076      * Implements the {@link javax.faces.event.ActionListener} interface.  Basically, this
1077      * method is used to listen for node selection events (when a user has clicked on a
1078      * leaf node.)
1079      *
1080      * @param event ActionEvent
1081      */
1082     public void setNodeSelected(ActionEvent event)
1083     {
1084         getDataModel().getTreeState().setSelected(getNodeId());
1085     }
1086 
1087     /**
1088      * Indicates whether or not the current {@link TreeNode} is selected.
1089      * @return boolean
1090      */
1091     public boolean isNodeSelected()
1092     {
1093         return (getNodeId() != null) ? getDataModel().getTreeState().isSelected(getNodeId()) : false;
1094     }
1095     
1096     /**
1097      * 
1098      * {@inheritDoc}
1099      * 
1100      * @since 2.0
1101      */
1102     public String createUniqueId(FacesContext context, String seed)
1103     {
1104         StringBuilder bld = new StringBuilder();
1105 
1106         // Generate an identifier for a component. The identifier will be prefixed with UNIQUE_ID_PREFIX, and will be unique within this UIViewRoot. 
1107         if(seed==null)
1108         {
1109             Long uniqueIdCounter = (Long) getStateHelper().get(PropertyKeys.uniqueIdCounter);
1110             uniqueIdCounter = (uniqueIdCounter == null) ? 0 : uniqueIdCounter;
1111             getStateHelper().put(PropertyKeys.uniqueIdCounter, (uniqueIdCounter+1L));
1112             return bld.append(UIViewRoot.UNIQUE_ID_PREFIX).append(uniqueIdCounter).toString();
1113         }
1114         // Optionally, a unique seed value can be supplied by component creators which should be included in the generated unique id.
1115         else
1116         {
1117             return bld.append(UIViewRoot.UNIQUE_ID_PREFIX).append(seed).toString();
1118         }
1119     }
1120     
1121     enum PropertyKeys
1122     {
1123         uniqueIdCounter
1124     }
1125 }