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.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.faces.application.FacesMessage;
29  import javax.faces.component.EditableValueHolder;
30  import javax.faces.component.NamingContainer;
31  import javax.faces.component.UIComponent;
32  import javax.faces.component.UIComponentBase;
33  import javax.faces.context.FacesContext;
34  import javax.faces.el.ValueBinding;
35  import javax.faces.event.AbortProcessingException;
36  import javax.faces.event.ActionEvent;
37  import javax.faces.event.FacesEvent;
38  import javax.faces.event.FacesListener;
39  import javax.faces.event.PhaseId;
40  
41  import org.apache.myfaces.shared_tomahawk.util.MessageUtils;
42  import org.apache.myfaces.tomahawk.util.Constants;
43  
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  
47  /**
48   * TreeData is a {@link UIComponent} that supports binding data stored in a tree represented
49   * by a {@link TreeNode} instance.  During iterative processing over the tree nodes in the
50   * data model, the object for the current node is exposed as a request attribute under the key
51   * specified by the <code>var</code> property.  {@link javax.faces.render.Renderer}s of this
52   * component should use the appropriate facet to assist in rendering.
53   *
54   * @JSFComponent
55   * @author Sean Schofield
56   * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
57   * @version $Revision: 940138 $ $Date: 2010-05-01 20:34:32 -0500 (Sat, 01 May 2010) $
58   */
59  public class UITreeData extends UIComponentBase implements NamingContainer, Tree {
60      private Log log = LogFactory.getLog(UITreeData.class);
61  
62      public static final String COMPONENT_TYPE = "org.apache.myfaces.UITree2";
63      public static final String COMPONENT_FAMILY = "org.apache.myfaces.HtmlTree2";
64      //private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.Tree2";
65      private static final String MISSING_NODE = "org.apache.myfaces.tree2.MISSING_NODE";
66      private static final int PROCESS_DECODES = 1;
67      private static final int PROCESS_VALIDATORS = 2;
68      private static final int PROCESS_UPDATES = 3;
69  
70      private TreeModel _cachedModel;
71      private String _nodeId;
72      private TreeNode _node;
73  
74      private Object _value;
75      private String _var;
76      private Map _saved = new HashMap();
77  
78      private TreeState _restoredState = null;
79  
80      /**
81       * Constructor
82       */
83      public UITreeData()
84      {
85          //setRendererType(DEFAULT_RENDERER_TYPE);
86      }
87  
88  
89      // see superclass for documentation
90      public String getFamily()
91      {
92          return COMPONENT_FAMILY;
93      }
94  
95      //  see superclass for documentation
96      public Object saveState(FacesContext context)
97      {
98          Object values[] = new Object[3];
99          values[0] = super.saveState(context);
100         values[1] = _var;
101         values[2] = _restoredState;
102         return ((Object) (values));
103     }
104 
105 
106     // see superclass for documentation
107     public void restoreState(FacesContext context, Object state)
108     {
109         Object values[] = (Object[]) state;
110         super.restoreState(context, values[0]);
111 
112         _var = (String)values[1];
113         _restoredState = (TreeState) values[2];
114     }
115 
116     public void encodeEnd(FacesContext context) throws IOException {
117         super.encodeEnd(context);
118 
119         // prepare to save the tree state -- fix for MYFACES-618
120         // should be done in saveState() but Sun RI does not call saveState() and restoreState()
121         // with javax.faces.STATE_SAVING_METHOD = server
122         TreeState state = getDataModel().getTreeState();
123         if ( state == null)
124         {
125             // the model supplier has forgotten to return a valid state manager, but we need one
126             state = new TreeStateBase();
127         }
128         // save the state with the component, unless it should explicitly not saved eg. session-scoped model and state
129         _restoredState = (state.isTransient()) ? null : state;
130 
131     }
132 
133     public void queueEvent(FacesEvent event)
134     {
135         super.queueEvent(new FacesEventWrapper(event, getNodeId(), this));
136     }
137 
138 
139     public void broadcast(FacesEvent event) throws AbortProcessingException
140     {
141         if (event instanceof FacesEventWrapper)
142         {
143             FacesEventWrapper childEvent = (FacesEventWrapper) event;
144             String currNodeId = getNodeId();
145             setNodeId(childEvent.getNodeId());
146             FacesEvent nodeEvent = childEvent.getFacesEvent();
147             nodeEvent.getComponent().broadcast(nodeEvent);
148             setNodeId(currNodeId);
149             return;
150         }
151         else if(event instanceof ToggleExpandedEvent)
152         {
153             ToggleExpandedEvent toggleEvent = (ToggleExpandedEvent) event;
154             String currentNodeId = getNodeId();
155             setNodeId(toggleEvent.getNodeId());
156             toggleExpanded();
157             setNodeId(currentNodeId);
158         }
159         else
160         {
161             super.broadcast(event);
162             return;
163         }
164     }
165 
166 
167     // see superclass for documentation
168     public void processDecodes(FacesContext context)
169     {
170         if (context == null) throw new NullPointerException("context");
171         if (!isRendered()) return;
172 
173         _cachedModel = null;
174         _saved = new HashMap();
175 
176         setNodeId(null);
177         decode(context);
178 
179         processNodes(context, PROCESS_DECODES, getDataModel().getTreeWalker());
180         // After processNodes is executed, the node active is the last one
181         // we have to set it to null again to avoid inconsistency on outsider
182         // code (just like UIData components does)
183         setNodeId(null);
184 
185     }
186 
187     // see superclass for documentation
188     public void processValidators(FacesContext context)
189     {
190         if (context == null) throw new NullPointerException("context");
191         if (!isRendered()) return;
192 
193         processNodes(context, PROCESS_VALIDATORS, getDataModel().getTreeWalker());
194 
195         setNodeId(null);
196     }
197 
198 
199     // see superclass for documentation
200     public void processUpdates(FacesContext context)
201     {
202         if (context == null) throw new NullPointerException("context");
203         if (!isRendered()) return;
204 
205         processNodes(context, PROCESS_UPDATES, getDataModel().getTreeWalker());
206 
207         setNodeId(null);
208     }
209 
210     // see superclass for documentation
211     public String getClientId(FacesContext context)
212     {
213         String ownClientId = super.getClientId(context);
214         if (_nodeId != null)
215         {
216             return ownClientId + NamingContainer.SEPARATOR_CHAR + _nodeId;
217         } else
218         {
219             return ownClientId;
220         }
221     }
222 
223     // see superclass for documentation
224     public void setValueBinding(String name, ValueBinding binding)
225     {
226         if ("value".equals(name))
227         {
228             _cachedModel = null;
229         } else if ("nodeVar".equals(name) || "nodeId".equals(name) || "treeVar".equals(name))
230         {
231             throw new IllegalArgumentException("name " + name);
232         }
233         super.setValueBinding(name, binding);
234     }
235 
236     // see superclass for documentation
237     public void encodeBegin(FacesContext context) throws IOException
238     {
239         /**
240          * The renderer will handle most of the encoding, but if there are any
241          * error messages queued for the components (validation errors), we
242          * do want to keep the saved state so that we can render the node with
243          * the invalid value.
244          */
245 
246         if (!keepSaved(context))
247         {
248             _saved = new HashMap();
249         }
250 
251         // FIX for MYFACES-404
252         // do not use the cached model the render phase
253         _cachedModel = null;
254 
255         super.encodeBegin(context);
256     }
257 
258     /**
259      * Sets the value of the TreeData.
260      *
261      * @param value The new value
262      *
263      * @deprecated
264      */
265     public void setValue(Object value)
266     {
267         _cachedModel = null;
268         _value = value;
269     }
270 
271 
272     /**
273      * Gets the model of the TreeData -
274      *  due to backwards-compatibility, this can also be retrieved by getValue.
275      *
276      * @return The value
277      */
278     public Object getModel()
279     {
280         return getValue();
281     }
282 
283     /**
284      * Sets the model of the TreeData -
285      *  due to backwards-compatibility, this can also be set by calling setValue.
286      *
287      * @param model The new model
288      */
289     public void setModel(Object model)
290     {
291         setValue(model);
292     }
293 
294 
295     /**
296      * Gets the value of the TreeData.
297      *
298      * @JSFProperty
299      *   required="true"
300      * @return The value
301      *
302      * @deprecated
303      */
304     public Object getValue()
305     {
306         if (_value != null) return _value;
307         ValueBinding vb = getValueBinding("value");
308         return vb != null ? vb.getValue(getFacesContext()) : null;
309     }
310 
311     /**
312      * Set the request-scope attribute under which the data object for the current node wil be exposed
313      * when iterating.
314      *
315      * @param var The new request-scope attribute name
316      */
317     public void setVar(String var)
318     {
319         _var = var;
320     }
321 
322 
323     /**
324      * Return the request-scope attribute under which the data object for the current node will be exposed
325      * when iterating. This property is not enabled for value binding expressions.
326      * 
327      * @JSFProperty
328      * @return The iterator attribute
329      */
330     public String getVar()
331     {
332         return _var;
333     }
334 
335     /**
336      * Calls through to the {@link TreeModel} and returns the current {@link TreeNode} or <code>null</code>.
337      *
338      * @return The current node
339      */
340     public TreeNode getNode()
341     {
342         return _node;
343     }
344 
345 
346     public String getNodeId()
347     {
348         return _nodeId;
349     }
350 
351 
352     public void setNodeId(String nodeId)
353     {
354         saveDescendantState();
355 
356         _nodeId = nodeId;
357 
358         TreeModel model = getDataModel();
359         if (model == null)
360         {
361             return;
362         }
363 
364         try
365         {
366             _node = model.getNodeById(nodeId);
367         }
368         //TODO: change to an own exception
369         catch (IndexOutOfBoundsException aob)
370         {
371             /**
372              * This might happen if we are trying to process a commandLink for a node that node that no longer
373              * exists.  Instead of allowing a RuntimeException to crash the application, we will add a warning
374              * message so the user can optionally display the warning.  Also, we will allow the user to provide
375              * their own value binding method to be called so they can handle it how they see fit.
376              */
377             FacesMessage message = MessageUtils.getMessageFromBundle(Constants.TOMAHAWK_DEFAULT_BUNDLE, MISSING_NODE, new String[] {nodeId});
378             message.setSeverity(FacesMessage.SEVERITY_WARN);
379             FacesContext.getCurrentInstance().addMessage(getId(), message);
380 
381             /** @todo call hook */
382             /** @todo figure out whether or not to abort this method gracefully */
383         }
384 
385         restoreDescendantState();
386 
387         if (_var != null)
388         {
389             Map requestMap = getFacesContext().getExternalContext().getRequestMap();
390 
391             if (nodeId == null)
392             {
393                 requestMap.remove(_var);
394             } else
395             {
396                 requestMap.put(_var, getNode());
397             }
398         }
399     }
400 
401     /**
402      * Gets an array of String containing the ID's of all of the {@link TreeNode}s in the path to
403      * the specified node.  The path information will be an array of <code>String</code> objects
404      * representing node ID's. The array will starting with the ID of the root node and end with
405      * the ID of the specified node.
406      *
407      * @param nodeId The id of the node for whom the path information is needed.
408      * @return String[]
409      */
410     public String[] getPathInformation(String nodeId)
411     {
412         return getDataModel().getPathInformation(nodeId);
413     }
414 
415     /**
416      * Indicates whether or not the specified {@link TreeNode} is the last child in the <code>List</code>
417      * of children.  If the node id provided corresponds to the root node, this returns <code>true</code>.
418      *
419      * @param nodeId The ID of the node to check
420      * @return boolean
421      */
422     public boolean isLastChild(String nodeId)
423     {
424         return getDataModel().isLastChild(nodeId);
425     }
426 
427     /**
428      * Returns a previously cached {@link TreeModel}, if any, or sets the cache variable to either the
429      * current value (if its a {@link TreeModel}) or to a new instance of {@link TreeModel} (if it's a
430      * {@link TreeNode}) with the provided value object as the root node.
431      *
432      * @return TreeModel
433      */
434     public TreeModel getDataModel()
435     {
436         if (_cachedModel != null)
437         {
438             return _cachedModel;
439         }
440 
441         Object value = getValue();
442         if (value != null)
443         {
444             if (value instanceof TreeModel)
445             {
446                 _cachedModel = (TreeModel) value;
447             }
448             else if (value instanceof TreeNode)
449             {
450                 _cachedModel = new TreeModelBase((TreeNode) value);
451             } else
452             {
453                 throw new IllegalArgumentException("Value must be a TreeModel or TreeNode");
454             }
455         }
456 
457         if (_restoredState != null)
458             _cachedModel.setTreeState(_restoredState); // set the restored state (if there is one) on the model
459 
460         return _cachedModel;
461     }
462 
463     /**
464      * Epands all nodes by default.
465      */
466     public void expandAll()
467     {
468         toggleAll(true);
469     }
470 
471     /**
472      * Collapse all nodes by default.
473      */
474     public void collapseAll()
475     {
476         toggleAll(false);
477     }
478 
479     /**
480      * Toggles all of the nodes to either expanded or collapsed depending on the
481      * parameter supplied.
482      *
483      * @param expanded Expand all of the nodes (a value of false indicates collapse
484      * all nodes)
485      */
486     private void toggleAll(boolean expanded)
487     {
488         TreeWalker walker = getDataModel().getTreeWalker();
489         walker.reset();
490 
491         TreeState state =  getDataModel().getTreeState();
492         walker.setCheckState(false);
493         walker.setTree(this);
494 
495         while(walker.next())
496         {
497             String id = getNodeId();
498             if ((expanded && !state.isNodeExpanded(id)) || (!expanded && state.isNodeExpanded(id)))
499             {
500                 state.toggleExpanded(id);
501             }
502         }
503     }
504 
505     /**
506      * Expands all of the nodes in the specfied path.
507      * @param nodePath The path to expand.
508      */
509     public void expandPath(String[] nodePath)
510     {
511         getDataModel().getTreeState().expandPath(nodePath);
512     }
513 
514     /**
515      * Expands all of the nodes in the specfied path.
516      * @param nodePath The path to expand.
517      */
518     public void collapsePath(String[] nodePath)
519     {
520         getDataModel().getTreeState().collapsePath(nodePath);
521     }
522 
523 
524     protected void processNodes(FacesContext context, int processAction, TreeWalker walker)
525     {
526         UIComponent facet = null;
527         walker.reset();
528         walker.setTree(this);
529 
530         while(walker.next())
531         {
532             TreeNode node = getNode();
533             facet = getFacet(node.getType());
534 
535             if (facet == null)
536             {
537                 log.warn("Unable to locate facet with the name: " + node.getType());
538                 continue;
539                 //throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
540             }
541 
542             switch (processAction)
543             {
544                 case PROCESS_DECODES:
545 
546                     facet.processDecodes(context);
547                     break;
548 
549                 case PROCESS_VALIDATORS:
550 
551                     facet.processValidators(context);
552                     break;
553 
554                 case PROCESS_UPDATES:
555 
556                     facet.processUpdates(context);
557                     break;
558             }
559         }
560 
561     }
562 
563     /**
564      * To support using input components for the nodes (e.g., input fields, checkboxes, and selection
565      * lists) while still only using one set of components for all nodes, the state held by the components
566      * for the current node must be saved for a new node is selected.
567      */
568     private void saveDescendantState()
569     {
570         FacesContext context = getFacesContext();
571         Iterator i = getFacets().values().iterator();
572         while (i.hasNext())
573         {
574             UIComponent facet = (UIComponent) i.next();
575             saveDescendantState(facet, context);
576         }
577     }
578 
579     /**
580      * Overloaded helper method for the no argument version of this method.
581      *
582      * @param component The component whose state needs to be saved
583      * @param context   FacesContext
584      */
585     private void saveDescendantState(UIComponent component, FacesContext context)
586     {
587         if (component instanceof EditableValueHolder)
588         {
589             EditableValueHolder input = (EditableValueHolder) component;
590             String clientId = component.getClientId(context);
591             SavedState state = (SavedState) _saved.get(clientId);
592             if (state == null)
593             {
594                 state = new SavedState();
595                 _saved.put(clientId, state);
596             }
597             state.setValue(input.getLocalValue());
598             state.setValid(input.isValid());
599             state.setSubmittedValue(input.getSubmittedValue());
600             state.setLocalValueSet(input.isLocalValueSet());
601         }
602 
603         List kids = component.getChildren();
604         for (int i = 0; i < kids.size(); i++)
605         {
606             saveDescendantState((UIComponent) kids.get(i), context);
607         }
608     }
609 
610 
611     /**
612      * Used to configure a new node with the state stored previously.
613      */
614     private void restoreDescendantState()
615     {
616         FacesContext context = getFacesContext();
617         Iterator i = getFacets().values().iterator();
618         while (i.hasNext())
619         {
620             UIComponent facet = (UIComponent) i.next();
621             restoreDescendantState(facet, context);
622         }
623     }
624 
625     /**
626      * Overloaded helper method for the no argument version of this method.
627      *
628      * @param component The component whose state needs to be restored
629      * @param context   FacesContext
630      */
631     private void restoreDescendantState(UIComponent component, FacesContext context)
632     {
633         String id = component.getId();
634         component.setId(id); // forces the cilent id to be reset
635 
636         if (component instanceof EditableValueHolder)
637         {
638             EditableValueHolder input = (EditableValueHolder) component;
639             String clientId = component.getClientId(context);
640             SavedState state = (SavedState) _saved.get(clientId);
641             if (state == null)
642             {
643                 state = new SavedState();
644             }
645             input.setValue(state.getValue());
646             input.setValid(state.isValid());
647             input.setSubmittedValue(state.getSubmittedValue());
648             input.setLocalValueSet(state.isLocalValueSet());
649         }
650 
651         List kids = component.getChildren();
652         for (int i = 0; i < kids.size(); i++)
653         {
654             restoreDescendantState((UIComponent)kids.get(i), context);
655         }
656         Map facets = component.getFacets();
657         for(Iterator i = facets.values().iterator(); i.hasNext();)
658         {
659             restoreDescendantState((UIComponent)i.next(), context);
660         }
661     }
662 
663     /**
664      * A regular bean with accessor methods for all state variables.
665      *
666      * @author Sean Schofield
667      * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
668      * @version $Revision: 940138 $ $Date: 2010-05-01 20:34:32 -0500 (Sat, 01 May 2010) $
669      */
670     private static class SavedState implements Serializable
671     {
672         private static final long serialVersionUID = 273343276957070557L;
673         private Object submittedValue;
674         private boolean valid = true;
675         private Object value;
676         private boolean localValueSet;
677 
678         Object getSubmittedValue()
679         {
680             return submittedValue;
681         }
682 
683         void setSubmittedValue(Object submittedValue)
684         {
685             this.submittedValue = submittedValue;
686         }
687 
688         boolean isValid()
689         {
690             return valid;
691         }
692 
693         void setValid(boolean valid)
694         {
695             this.valid = valid;
696         }
697 
698         Object getValue()
699         {
700             return value;
701         }
702 
703         void setValue(Object value)
704         {
705             this.value = value;
706         }
707 
708         boolean isLocalValueSet()
709         {
710             return localValueSet;
711         }
712 
713         void setLocalValueSet(boolean localValueSet)
714         {
715             this.localValueSet = localValueSet;
716         }
717     }
718 
719     /**
720      * Inner class used to wrap the original events produced by child components in the tree.
721      * This will allow the tree to find the appropriate component later when its time to
722      * broadcast the events to registered listeners.  Code is based on a similar private
723      * class for UIData.
724      */
725     private static class FacesEventWrapper extends FacesEvent
726     {
727         private static final long serialVersionUID = -3056153249469828447L;
728         private FacesEvent _wrappedFacesEvent;
729         private String _nodeId;
730 
731 
732         public FacesEventWrapper(FacesEvent facesEvent, String nodeId, UIComponent component)
733         {
734             super(component);
735             _wrappedFacesEvent = facesEvent;
736             _nodeId = nodeId;
737         }
738 
739 
740         public PhaseId getPhaseId()
741         {
742             return _wrappedFacesEvent.getPhaseId();
743         }
744 
745 
746         public void setPhaseId(PhaseId phaseId)
747         {
748             _wrappedFacesEvent.setPhaseId(phaseId);
749         }
750 
751 
752         public void queue()
753         {
754             _wrappedFacesEvent.queue();
755         }
756 
757 
758         public String toString()
759         {
760             return _wrappedFacesEvent.toString();
761         }
762 
763 
764         public boolean isAppropriateListener(FacesListener faceslistener)
765         {
766             // this event type is only intended for wrapping a real event
767             return false;
768         }
769 
770 
771         public void processListener(FacesListener faceslistener)
772         {
773             throw new UnsupportedOperationException("This event type is only intended for wrapping a real event");
774         }
775 
776 
777         public FacesEvent getFacesEvent()
778         {
779             return _wrappedFacesEvent;
780         }
781 
782 
783         public String getNodeId()
784         {
785             return _nodeId;
786         }
787     }
788 
789     /**
790      * Returns true if there is an error message queued for at least one of the nodes.
791      *
792      * @param context FacesContext
793      * @return whether an error message is present
794      */
795     private boolean keepSaved(FacesContext context)
796     {
797         Iterator clientIds = _saved.keySet().iterator();
798         while (clientIds.hasNext())
799         {
800             String clientId = (String) clientIds.next();
801             Iterator messages = context.getMessages(clientId);
802             while (messages.hasNext())
803             {
804                 FacesMessage message = (FacesMessage) messages.next();
805                 if (message.getSeverity().compareTo(FacesMessage.SEVERITY_ERROR) >= 0)
806                 {
807                     return true;
808                 }
809             }
810         }
811 
812         return false;
813     }
814 
815     /**
816      * Toggle the expanded state of the current node.
817      */
818     public void toggleExpanded()
819     {
820         getDataModel().getTreeState().toggleExpanded(getNodeId());
821     }
822 
823     /**
824      * Indicates whether or not the current {@link TreeNode} is expanded.
825      * @return boolean
826      */
827     public boolean isNodeExpanded()
828     {
829         return getDataModel().getTreeState().isNodeExpanded(getNodeId());
830     }
831 
832     /**
833      * Implements the {@link javax.faces.event.ActionListener} interface.  Basically, this
834      * method is used to listen for node selection events (when a user has clicked on a
835      * leaf node.)
836      *
837      * @param event ActionEvent
838      */
839     public void setNodeSelected(ActionEvent event)
840     {
841         getDataModel().getTreeState().setSelected(getNodeId());
842     }
843 
844     /**
845      * Indicates whether or not the current {@link TreeNode} is selected.
846      * @return boolean
847      */
848     public boolean isNodeSelected()
849     {
850         return (getNodeId() != null) ? getDataModel().getTreeState().isSelected(getNodeId()) : false;
851     }
852 }