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.model;
20  
21  import java.lang.reflect.Array;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.List;
25  
26  import javax.faces.model.DataModel;
27  
28  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
29  import org.apache.myfaces.trinidad.util.CollectionUtils;
30  
31  /**
32   * Creates a TreeModel from a List of beans.
33   * To use this class you must have a tree of beans (or Maps).
34   * The structure of your tree must be a List (or array) of root beans, and each
35   * bean must have a getter method (or Map property) that returns the 
36   * children List. All elements of your tree must be the same type.
37   * <P>
38   * Suppose you have a bean called EmpBean that contains the data
39   * for a particular employee. Suppose this bean has a method called
40   * getDirectReports() that returns a List of EmpBeans which are the direct 
41   * reports of the employee. Suppose there is a List called "founders" which
42   * is the root list of EmpBeans. Now you can construct a TreeModel by calling
43   * <pre>
44   * TreeModel model = new ChildPropertyTreeModel(founders, "directReports");
45   * </pre>
46   * Bean rules will be used to find an appropriate getter method for the 
47   * "directReports" property. 
48   * java.util.Maps are also supported instead of beans.
49   * <p>
50   * Example: Given the following class:
51   * <pre>
52   * public class Person
53   * {
54   *    public Person(String name)
55   *    {
56   *      _name = name;
57   *    }
58   *    
59   *    public String getName()
60   *    {
61   *      return _name;
62   *    }
63   *    
64   *    public List getKids()
65   *    {
66   *      return _kids;
67   *    }
68   *    
69   *    private final String _name;
70   *    private final List _kids = new ArrayList();
71   * }
72   * </pre>
73   * You can construct a tree by:
74   * <pre>
75   * Person john = new Person("John Smith");
76   * Person kim = new Person("Kim Smith");
77   * Person tom = new Person("Tom Smith");
78   * Person ira = new Person("Ira Wickrememsinghe");
79   * Person mallika = new Person("Mallika Wickremesinghe");
80   * 
81   * john.getKids().add(kim);
82   * john.getKids().add(tom);
83   * ira.getKids().add(mallika);
84   * 
85   * // create the list of root nodes:
86   * List people = new ArrayList();
87   * people.add(john);
88   * people.add(ira);
89   * </pre>
90   * Now you can construct a TreeModel by:
91   * <pre>
92   * TreeModel model = new ChildPropertyTreeModel(people, "kids");
93   * </pre>
94   */
95  public class ChildPropertyTreeModel extends TreeModel
96  {
97  
98    /**
99     * Creates a TreeModel
100    * @param instance The Collection of root nodes of this tree.
101    * This can be a List or array of beans (or Maps).
102    * This instance is first converted into a CollectionModel (see
103    * {@link ModelUtils#toCollectionModel}).
104    * @param childProperty This property will be used to get at the child Lists
105    * from each bean (or Map). Bean rules will be used to find a getter method
106    * that matches this property. If each node is a Map, this property will be
107    * passed in to the Map's get method to get the child List. 
108    */
109   public ChildPropertyTreeModel(Object instance, String childProperty)
110   {
111     this();
112     setChildProperty(childProperty);
113     setWrappedData(instance);
114   }
115 
116   /**
117    * No-arg constructor for use with managed-beans.
118    * Must call the {@link #setChildProperty} and
119    * {@link #setWrappedData} methods after constructing this instance.
120    */
121   public ChildPropertyTreeModel()
122   {
123     Node root = new Node(null);
124     _path.add(root);
125   }
126 
127   /**
128    * Gets the rowKey of the current row.
129    */
130   @Override
131   public Object getRowKey()
132   {
133     final int sz = _path.size() - 1;
134     Object lastRowkey = _getRowKey();
135     if ((sz == 0) && (lastRowkey == null))
136       return null;  // root collection
137     
138     // have to clone the path here. otherwise, we have to say that
139     // this tree model cannot be mutated while accessing the path
140     // returned by this method.
141     List<Object> path = new ArrayList<Object>(sz+1);
142     if (sz > 0)
143     {
144       for(int i=0; i<sz; i++)
145       {
146         Node node = _getNode(i);
147         path.add(node.childModel.getRowKey());
148       }
149     }
150     path.add(lastRowkey);
151     return path;
152   }
153 
154   /**
155    * Selects a new current row. The row that matches the given rowKey
156    * is made current.
157    * @param rowKey use null to access the root collection 
158    */
159   @SuppressWarnings("unchecked")
160   @Override
161   public void setRowKey(Object rowKey)
162   {
163     Node root = _getNode(0);
164     _path.clear();
165     _path.add(root);
166     
167     List<Object> path = (List<Object>) rowKey;
168     if ((path == null) || (path.size() == 0))
169     {
170       setRowIndex(-1);
171       return;
172     }
173       
174     int lastIndex = path.size() - 1;
175     for(int i=0; i<lastIndex; i++)
176     {
177       Object pathKey = path.get(i);
178       _setRowKey(pathKey);
179       if (!isRowAvailable())
180       {
181         // setRowKey should fail for invalid row key in the path.
182         // Also isRowAvailable should return false
183         _path.clear();
184         _path.add(root);
185         setRowIndex(-1);
186         return;        
187       }
188       enterContainer();
189     }
190     
191     _setRowKey(path.get(lastIndex));
192   }
193 
194   @SuppressWarnings("unchecked")
195   @Override
196   public Object getContainerRowKey(Object childKey)
197   {
198     List<Object> path = (List<Object>) childKey;
199     if ((path == null) || (path.size() <= 1))
200       return null;
201 
202     // wrap sublist in a Serializable copy, since sublist usually returns non-Serializable
203     // instances
204     return CollectionUtils.newSerializableList(path.subList(0, path.size() - 1));
205   }
206 
207   @Override
208   public int getRowCount()
209   {
210     return _getModel().getRowCount();
211   }
212 
213   @Override
214   public Object getRowData()
215   {
216     return _getModel().getRowData();
217   }
218 
219   @Override
220   public boolean isRowAvailable()
221   {
222     return _getModel().isRowAvailable();
223   }
224 
225   @Override
226   public boolean isContainer()
227   {
228     Object rowData = getRowData();
229     Object value = getChildData(rowData);
230     
231     if (value != null)
232     {
233       if (value instanceof Collection<?>)
234       {
235         return !((Collection<?>)value).isEmpty();
236       }
237       else if (value.getClass().isArray())
238       {
239         return Array.getLength(value) > 0;
240       }
241       else if (value instanceof DataModel)
242       {
243         return ((DataModel)value).getRowCount() > 0;
244       }
245     }
246     
247     return value != null;
248   }
249 
250   @Override
251   public void enterContainer()
252   {
253     Object rowData = getRowData();
254     if (rowData == null)
255       throw new IllegalStateException(_LOG.getMessage(
256         "NULL_ROWDATA"));
257     Node node = new Node(rowData);
258     _path.add(node);
259   }
260 
261   @Override
262   public void exitContainer()
263   {
264     int sz = _path.size();
265     if (sz > 1)
266       _path.remove(sz - 1);
267     else
268       throw new IllegalStateException(_LOG.getMessage(
269         "CANNOT_EXIT_ROOT_CONTAINER"));
270   }
271   
272   /**
273    * Gets the instance being wrapped by this TreeModel.
274    */
275   @Override
276   public Object getWrappedData()
277   {
278     return _wrappedData;
279   }
280 
281   /**
282    * Sets the instance being wrapped by this TreeModel.
283    * Calling this method sets the path to empty.
284    */
285   @Override
286   public void setWrappedData(Object data)
287   {
288     Node root = _getNode(0);
289     root.childModel = ModelUtils.toCollectionModel(data);
290     setRowKey(null);
291     _wrappedData = data;
292   }
293 
294   /**
295    * Gets the property name used to fetch the children.
296    */
297   public final String getChildProperty()
298   {
299     return _childProperty;
300   }
301 
302   /**
303    * Sets the property name used to fetch the children.
304    */
305   public final void setChildProperty(String childProperty)
306   {
307     _childProperty = childProperty;
308   }
309 
310   @Override
311   public int getRowIndex()
312   {
313     return _getModel().getRowIndex();
314   }
315 
316   @Override
317   public void setRowIndex(int rowIndex)
318   {
319     _getModel().setRowIndex(rowIndex);
320   }
321 
322   @Override
323   public boolean isSortable(String property)
324   {
325     return _getModel().isSortable(property);
326   }
327 
328   @Override
329   public List<SortCriterion> getSortCriteria()
330   {
331     return _getModel().getSortCriteria();
332   }
333 
334   @Override
335   public void setSortCriteria(List<SortCriterion> criteria)
336   {
337     _getModel().setSortCriteria(criteria);
338   }
339 
340   /**
341    * Gets the child data for a node. This child data will be
342    * converted into a CollectionModel by calling
343    * {@link #createChildModel}.
344    * @param parentData the node to get the child data from
345    * @return the List or array that is the child data.
346    * must return null for a leaf node.
347    */
348   protected Object getChildData(Object parentData)
349   {
350     String prop = getChildProperty();
351     if (prop == null)
352       return null;
353     
354     return SortableModel.__resolveProperty(parentData, prop);
355   }
356 
357   /**
358    * Converts childData into a CollectionModel.
359    * This method calls {@link ModelUtils#toCollectionModel}
360    * @param childData the data to convert. This can be a List or array.
361    */
362   protected CollectionModel createChildModel(Object childData)
363   {
364     CollectionModel model = ModelUtils.toCollectionModel(childData);
365     model.setRowIndex(-1);
366     return model;
367   }
368 
369   private Object _getRowKey()
370   {
371     return _getModel().getRowKey();
372   }
373 
374   private void _setRowKey(Object key)
375   {
376     _getModel().setRowKey(key);
377   }
378   
379   private Node _getCurrentNode()
380   {
381     return _getNode(_path.size() - 1);
382   }
383 
384   private Node _getNode(int index)
385   {
386     return _path.get(index);    
387   }
388 
389   private CollectionModel _getModel()
390   {
391     Node node = _getCurrentNode();
392     CollectionModel model = node.childModel;
393   
394     if (model == null)
395     {
396       Object value = getChildData(node.parentData);
397       model = createChildModel(value);
398       node.childModel = model;
399     }
400     return model;
401   }
402   
403   private static final class Node
404   {
405     public final Object parentData;
406     public CollectionModel childModel = null;
407 
408     public Node(Object parentData)
409     {
410       this.parentData = parentData;
411     }
412   }
413 
414   private final List<Node> _path = new ArrayList<Node>(5);
415   private String _childProperty = null;
416   private Object _wrappedData = null;
417   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(
418     ChildPropertyTreeModel.class);
419 }