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       enterContainer();
180     }
181     
182     _setRowKey(path.get(lastIndex));
183   }
184 
185   @SuppressWarnings("unchecked")
186   @Override
187   public Object getContainerRowKey(Object childKey)
188   {
189     List<Object> path = (List<Object>) childKey;
190     if ((path == null) || (path.size() <= 1))
191       return null;
192 
193     // wrap sublist in a Serializable copy, since sublist usually returns non-Serializable
194     // instances
195     return CollectionUtils.newSerializableList(path.subList(0, path.size() - 1));
196   }
197 
198   @Override
199   public int getRowCount()
200   {
201     return _getModel().getRowCount();
202   }
203 
204   @Override
205   public Object getRowData()
206   {
207     return _getModel().getRowData();
208   }
209 
210   @Override
211   public boolean isRowAvailable()
212   {
213     return _getModel().isRowAvailable();
214   }
215 
216   @Override
217   public boolean isContainer()
218   {
219     Object rowData = getRowData();
220     Object value = getChildData(rowData);
221     
222     if (value != null)
223     {
224       if (value instanceof Collection<?>)
225       {
226         return !((Collection<?>)value).isEmpty();
227       }
228       else if (value.getClass().isArray())
229       {
230         return Array.getLength(value) > 0;
231       }
232       else if (value instanceof DataModel)
233       {
234         return ((DataModel)value).getRowCount() > 0;
235       }
236     }
237     
238     return value != null;
239   }
240 
241   @Override
242   public void enterContainer()
243   {
244     Object rowData = getRowData();
245     if (rowData == null)
246       throw new IllegalStateException(_LOG.getMessage(
247         "NULL_ROWDATA"));
248     Node node = new Node(rowData);
249     _path.add(node);
250   }
251 
252   @Override
253   public void exitContainer()
254   {
255     int sz = _path.size();
256     if (sz > 1)
257       _path.remove(sz - 1);
258     else
259       throw new IllegalStateException(_LOG.getMessage(
260         "CANNOT_EXIT_ROOT_CONTAINER"));
261   }
262   
263   /**
264    * Gets the instance being wrapped by this TreeModel.
265    */
266   @Override
267   public Object getWrappedData()
268   {
269     return _wrappedData;
270   }
271 
272   /**
273    * Sets the instance being wrapped by this TreeModel.
274    * Calling this method sets the path to empty.
275    */
276   @Override
277   public void setWrappedData(Object data)
278   {
279     Node root = _getNode(0);
280     root.childModel = ModelUtils.toCollectionModel(data);
281     setRowKey(null);
282     _wrappedData = data;
283   }
284 
285   /**
286    * Gets the property name used to fetch the children.
287    */
288   public final String getChildProperty()
289   {
290     return _childProperty;
291   }
292 
293   /**
294    * Sets the property name used to fetch the children.
295    */
296   public final void setChildProperty(String childProperty)
297   {
298     _childProperty = childProperty;
299   }
300 
301   @Override
302   public int getRowIndex()
303   {
304     return _getModel().getRowIndex();
305   }
306 
307   @Override
308   public void setRowIndex(int rowIndex)
309   {
310     _getModel().setRowIndex(rowIndex);
311   }
312 
313   @Override
314   public boolean isSortable(String property)
315   {
316     return _getModel().isSortable(property);
317   }
318 
319   @Override
320   public List<SortCriterion> getSortCriteria()
321   {
322     return _getModel().getSortCriteria();
323   }
324 
325   @Override
326   public void setSortCriteria(List<SortCriterion> criteria)
327   {
328     _getModel().setSortCriteria(criteria);
329   }
330 
331   /**
332    * Gets the child data for a node. This child data will be
333    * converted into a CollectionModel by calling
334    * {@link #createChildModel}.
335    * @param parentData the node to get the child data from
336    * @return the List or array that is the child data.
337    * must return null for a leaf node.
338    */
339   protected Object getChildData(Object parentData)
340   {
341     String prop = getChildProperty();
342     if (prop == null)
343       return null;
344     
345     return SortableModel.__resolveProperty(parentData, prop);
346   }
347 
348   /**
349    * Converts childData into a CollectionModel.
350    * This method calls {@link ModelUtils#toCollectionModel}
351    * @param childData the data to convert. This can be a List or array.
352    */
353   protected CollectionModel createChildModel(Object childData)
354   {
355     CollectionModel model = ModelUtils.toCollectionModel(childData);
356     model.setRowIndex(-1);
357     return model;
358   }
359 
360   private Object _getRowKey()
361   {
362     return _getModel().getRowKey();
363   }
364 
365   private void _setRowKey(Object key)
366   {
367     _getModel().setRowKey(key);
368   }
369   
370   private Node _getCurrentNode()
371   {
372     return _getNode(_path.size() - 1);
373   }
374 
375   private Node _getNode(int index)
376   {
377     return _path.get(index);    
378   }
379 
380   private CollectionModel _getModel()
381   {
382     Node node = _getCurrentNode();
383     CollectionModel model = node.childModel;
384   
385     if (model == null)
386     {
387       Object value = getChildData(node.parentData);
388       model = createChildModel(value);
389       node.childModel = model;
390     }
391     return model;
392   }
393   
394   private static final class Node
395   {
396     public final Object parentData;
397     public CollectionModel childModel = null;
398 
399     public Node(Object parentData)
400     {
401       this.parentData = parentData;
402     }
403   }
404 
405   private final List<Node> _path = new ArrayList<Node>(5);
406   private String _childProperty = null;
407   private Object _wrappedData = null;
408   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(
409     ChildPropertyTreeModel.class);
410 }