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.crosstable;
20  
21  import java.sql.ResultSet;
22  import java.util.ArrayList;
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.application.FacesMessage;
30  import javax.faces.component.EditableValueHolder;
31  import javax.faces.component.UIComponent;
32  import javax.faces.component.UIData;
33  import javax.faces.context.FacesContext;
34  import javax.faces.el.ValueBinding;
35  import javax.faces.model.ArrayDataModel;
36  import javax.faces.model.DataModel;
37  import javax.faces.model.ListDataModel;
38  import javax.faces.model.ResultDataModel;
39  import javax.faces.model.ResultSetDataModel;
40  import javax.faces.model.ScalarDataModel;
41  import javax.servlet.jsp.jstl.sql.Result;
42  
43  /**
44   * An object which can be a child of a t:dataTable, and inserts a dynamic
45   * set of columns into the parent table. The set of columns inserted are
46   * defined by a collection returned from a value-binding.
47   * <p>
48   * Class org.apache.myfaces.component.html.ext.HtmlDataTable (aka t:dataTable)
49   * has special-case code to detect a child of this type, and invoke the
50   * necessary methods on this class. This class does not work with other
51   * UIData implementations. There is no renderer for this component; the
52   * HtmlTableRenderer associated with ext.HtmlDataTable is UIColumns-aware,
53   * and implements the necessary logic itself.
54   * <p>
55   * This class is actually a UIData itself, which is effectively "merged"
56   * with the parent HtmlDataTable. It can't be used as a stand-alone table,
57   * however, because it uses the DataModel returned by the "value" value-binding
58   * to define the columns, and depends on the parent table's "value" 
59   * value-binding to define the row DataModel.
60   * <p>
61   * The "value" attribute of this class must be a value-binding which
62   * returns a DataModel of objects (or a List, Array or ResultSet which
63   * automatically gets wrapped in the appropriate DataModel). 
64   * <p>
65   * In a normal table, each UIColumn child has facets and child components
66   * which define that column. In this component, the same child components
67   * apply to each column (ie are repeated dataModel.size times). However
68   * as the columns are rendered, the current DataModel object (ie the object
69   * defining the current column) is stored into a variable whose name
70   * is defined by attribute "var". This allows the child components of
71   * this component to refer to attributes on those objects to set things
72   * like the current column's name. When the objects must be rendered as
73   * different components (eg h:outputText or h:outputDate), multiple
74   * child components can be used with rendered attributes selecting the
75   * appropriate one depending upon the current column object's data.
76   * 
77   * @JSFComponent
78   * 
79   * @author Mathias Broekelmann (latest modification by $Author: lu4242 $)
80   * @version $Revision: 659874 $ $Date: 2008-05-24 15:59:15 -0500 (Sat, 24 May 2008) $
81   */
82  public class UIColumns extends UIData {
83      public static final String COMPONENT_TYPE = "org.apache.myfaces.Columns";
84  
85      private static final Class OBJECT_ARRAY_CLASS = (new Object[0]).getClass();
86  
87      private static final int PROCESS_DECODES = 1;
88      private static final int PROCESS_VALIDATORS = 2;
89      private static final int PROCESS_UPDATES = 3;
90  
91      private boolean _isValidChilds = true;
92  
93      // holds the the state for each cell of the child components of this UIColumns
94      private Map _cellStates = new HashMap();
95  
96      private Object _initialDescendantComponentState = null;
97  
98      private int _colIndex = -1;
99      private UIData _parentUIData;
100 
101     private Map _dataModelMap = new HashMap();
102 
103     public UIColumns() {
104         super();
105     }
106 
107     /**
108      * Return true if there are any more <i>columns</i> to process.
109      */
110     public boolean isRowAvailable() {
111         return getDataModel().isRowAvailable();
112     }
113 
114     /**
115      * Get the number of dynamic columns represented by this instance.
116      */
117     public int getRowCount() {
118         return getDataModel().getRowCount();
119     }
120 
121     /** Get the object representing the current column. */
122     public Object getRowData() {
123         return getDataModel().getRowData();
124     }
125 
126     /** Get the currently selected column index. */
127     public int getRowIndex() {
128         return _colIndex;
129     }
130 
131     /** Set the currently selected column. */
132     public void setRowIndex(int colIndex) {
133         if (colIndex < -1) {
134             throw new IllegalArgumentException("colIndex is less than -1");
135         }
136 
137         if (_colIndex == colIndex) {
138             return;
139         }
140 
141         FacesContext facesContext = getFacesContext();
142 
143         if (_colIndex == -1) {
144             if (_initialDescendantComponentState == null) {
145                 _initialDescendantComponentState = saveDescendantComponentStates(getFacetsAndChildren());
146             }
147         } else {
148             _cellStates.put(getClientId(facesContext), saveDescendantComponentStates(getFacetsAndChildren()));
149         }
150 
151         _colIndex = colIndex;
152 
153         DataModel dataModel = getDataModel();
154         dataModel.setRowIndex(colIndex);
155 
156         String var = getVar();
157         if (colIndex == -1) {
158             if (var != null) {
159                 facesContext.getExternalContext().getRequestMap().remove(var);
160             }
161         } else {
162             if (var != null) {
163                 if (isRowAvailable()) {
164                     Object rowData = dataModel.getRowData();
165                     facesContext.getExternalContext().getRequestMap().put(var, rowData);
166                 } else {
167                     facesContext.getExternalContext().getRequestMap().remove(var);
168                 }
169             }
170         }
171 
172         if (_colIndex == -1) {
173             restoreDescendantComponentStates(getFacetsAndChildren(), _initialDescendantComponentState);
174         } else {
175             Object rowState = _cellStates.get(getClientId(facesContext));
176             if (rowState == null) {
177                 restoreDescendantComponentStates(getFacetsAndChildren(), _initialDescendantComponentState);
178             } else {
179                 restoreDescendantComponentStates(getFacetsAndChildren(), rowState);
180             }
181         }
182     }
183 
184     protected void restoreDescendantComponentStates(Iterator childIterator, Object state) {
185         Iterator descendantStateIterator = null;
186         while (childIterator.hasNext()) {
187             if (descendantStateIterator == null && state != null) {
188                 descendantStateIterator = ((Collection) state).iterator();
189             }
190             UIComponent component = (UIComponent) childIterator.next();
191             // reset the client id (see spec 3.1.6)
192             component.setId(component.getId());
193             if (!component.isTransient()) {
194                 Object childState = null;
195                 Object descendantState = null;
196                 if (descendantStateIterator != null && descendantStateIterator.hasNext()) {
197                     Object[] object = (Object[]) descendantStateIterator.next();
198                     childState = object[0];
199                     descendantState = object[1];
200                 }
201                 if (component instanceof EditableValueHolder) {
202                     ((EditableValueHolderState) childState).restoreState((EditableValueHolder) component);
203                 }
204                 restoreDescendantComponentStates(component.getFacetsAndChildren(), descendantState);
205             }
206         }
207     }
208 
209     protected Object saveDescendantComponentStates(Iterator childIterator) {
210         Collection childStates = null;
211         while (childIterator.hasNext()) {
212             if (childStates == null) {
213                 childStates = new ArrayList();
214             }
215             UIComponent child = (UIComponent) childIterator.next();
216             if (!child.isTransient()) {
217                 Object descendantState = saveDescendantComponentStates(child.getFacetsAndChildren());
218                 Object state = null;
219                 if (child instanceof EditableValueHolder) {
220                     state = new EditableValueHolderState((EditableValueHolder) child);
221                 }
222                 childStates.add(new Object[] { state, descendantState });
223             }
224         }
225         return childStates;
226     }
227 
228     public void setValue(Object value) {
229         super.setValue(value);
230         _dataModelMap.clear();
231         _cellStates.clear();
232         _isValidChilds = true;
233     }
234 
235     public void setValueBinding(String name, ValueBinding binding) {
236         if (name == null) {
237             throw new NullPointerException("name");
238         } else if (name.equals("value")) {
239             _dataModelMap.clear();
240         } else if (name.equals("var") || name.equals("rowIndex")) {
241             throw new IllegalArgumentException("name " + name);
242         }
243         super.setValueBinding(name, binding);
244     }
245 
246     /**
247      * Get a DataModel whose wrapped data contains a collection of
248      * objects representing columns.
249      */
250     protected DataModel getDataModel() {
251         String clientID = "";
252         UIComponent parent = getParentUIData().getParent();
253         if (parent != null) {
254             clientID = parent.getClientId(getFacesContext());
255         }
256         DataModel dataModel = (DataModel) _dataModelMap.get(clientID);
257         if (dataModel == null) {
258             dataModel = createDataModel();
259             _dataModelMap.put(clientID, dataModel);
260         }
261         return dataModel;
262     }
263 
264     protected void setDataModel(DataModel dataModel) {
265         if (dataModel == null && getParent() == null) {
266             return;
267         }
268         UIComponent parent = getParentUIData().getParent();
269         String clientID = "";
270         if (parent != null) {
271             clientID = parent.getClientId(getFacesContext());
272         }
273         _dataModelMap.put(clientID, dataModel);
274     }
275 
276     /**
277      * Creates a new DataModel around the current value.
278      */
279     protected DataModel createDataModel() {
280         Object value = getValue();
281         if (value == null) {
282             return EMPTY_DATA_MODEL;
283         } else if (value instanceof DataModel) {
284             return (DataModel) value;
285         } else if (value instanceof List) {
286             return new ListDataModel((List) value);
287         } else if (value instanceof Collection) {
288             return new ListDataModel(new ArrayList((Collection) value));
289         } else if (OBJECT_ARRAY_CLASS.isAssignableFrom(value.getClass())) {
290             return new ArrayDataModel((Object[]) value);
291         } else if (value instanceof ResultSet) {
292             return new ResultSetDataModel((ResultSet) value);
293         } else if (value instanceof Result) {
294             return new ResultDataModel((Result) value);
295         } else {
296             return new ScalarDataModel(value);
297         }
298     }
299 
300     private static final DataModel EMPTY_DATA_MODEL = new DataModel() {
301         public boolean isRowAvailable() {
302             return false;
303         }
304 
305         public int getRowCount() {
306             return 0;
307         }
308 
309         public Object getRowData() {
310             throw new IllegalArgumentException();
311         }
312 
313         public int getRowIndex() {
314             return -1;
315         }
316 
317         public void setRowIndex(int i) {
318             if (i < -1)
319                 throw new IndexOutOfBoundsException("Index < 0 : " + i);
320         }
321 
322         public Object getWrappedData() {
323             return null;
324         }
325 
326         public void setWrappedData(Object obj) {
327             if (obj == null)
328                 return; //Clearing is allowed
329             throw new UnsupportedOperationException(this.getClass().getName() + " UnsupportedOperationException");
330         }
331     };
332 
333     /**
334      * Update the input component (if any) in each dynamic column.
335      * 
336      * @see javax.faces.component.UIData#processDecodes(javax.faces.context.FacesContext)
337      */
338     public void processDecodes(FacesContext context) {
339         if (context == null)
340             throw new NullPointerException("context");
341         if (!isRendered())
342             return;
343 
344         setRowIndex(-1);
345         processColumnsFacets(context, PROCESS_DECODES);
346         processRows(context, PROCESS_DECODES);
347         setRowIndex(-1);
348 
349         try {
350             decode(context);
351         } catch (RuntimeException e) {
352             context.renderResponse();
353             throw e;
354         }
355     }
356 
357     private void processColumnsFacets(FacesContext context, int processAction) {
358         int first = getFirst();
359         int cols = getRows();
360         int last;
361         if (cols == 0) {
362             last = getRowCount();
363         } else {
364             last = first + cols;
365         }
366 
367         for (int colIndex = first; colIndex < last; colIndex++) {
368             setRowIndex(colIndex);
369             if (isRowAvailable()) {
370                 for (Iterator facetsIter = getFacets().values().iterator(); facetsIter.hasNext();) {
371                     UIComponent facet = (UIComponent) facetsIter.next();
372                     process(context, facet, processAction);
373                 }
374             }
375         }
376         setRowIndex(-1);
377     }
378 
379     private void processRows(FacesContext context, int processAction) {
380         UIData parentUIData = getParentUIData();
381         int first = parentUIData.getFirst();
382         int rows = parentUIData.getRows();
383         int last;
384         if (rows == 0) {
385             last = parentUIData.getRowCount();
386         } else {
387             last = first + rows;
388         }
389 
390         for (int rowIndex = first; rowIndex < last; rowIndex++) {
391             parentUIData.setRowIndex(rowIndex);
392             if (parentUIData.isRowAvailable()) {
393                 processColumns(context, processAction);
394             }
395         }
396     }
397 
398     /**
399      * Return the UIData this component is nested within.
400      * <p>
401      * Actually, this component will only function correctly when nested within
402      * an org.apache.myfaces.component.html.ext.HtmlDataTable.
403      */
404     private UIData getParentUIData() {
405         if (_parentUIData == null) {
406             UIComponent parent = getParent();
407             if (!(parent instanceof UIData)) {
408                 throw new IllegalStateException("UIColumns component must be a child of a UIData component");
409             }
410             _parentUIData = (UIData) parent;
411         }
412         return _parentUIData;
413     }
414 
415     private void processColumns(FacesContext context, int processAction) {
416         int first = getFirst();
417         int cols = getRows();
418         int last;
419         if (cols == 0) {
420             last = getRowCount();
421         } else {
422             last = first + cols;
423         }
424 
425         for (int colIndex = first; colIndex < last; colIndex++) {
426             setRowIndex(colIndex);
427             if (isRowAvailable()) {
428                 for (Iterator columnChildIter = getChildren().iterator(); columnChildIter.hasNext();) {
429                     UIComponent columnChild = (UIComponent) columnChildIter.next();
430                     process(context, columnChild, processAction);
431                 }
432             }
433         }
434         setRowIndex(-1);
435     }
436 
437     /**
438      * @see javax.faces.component.UIData#processValidators(javax.faces.context.FacesContext)
439      */
440     public void processValidators(FacesContext context) {
441         if (context == null)
442             throw new NullPointerException("context");
443         if (!isRendered())
444             return;
445         setRowIndex(-1);
446         processColumnsFacets(context, PROCESS_VALIDATORS);
447         processRows(context, PROCESS_VALIDATORS);
448         setRowIndex(-1);
449 
450         // check if an validation error forces the render response for our data
451         if (context.getRenderResponse()) {
452             _isValidChilds = false;
453         }
454     }
455 
456     /**
457      * @see javax.faces.component.UIData#processUpdates(javax.faces.context.FacesContext)
458      */
459     public void processUpdates(FacesContext context) {
460         if (context == null)
461             throw new NullPointerException("context");
462         if (!isRendered())
463             return;
464         setRowIndex(-1);
465         processColumnsFacets(context, PROCESS_UPDATES);
466         processRows(context, PROCESS_UPDATES);
467         setRowIndex(-1);
468 
469         // check if an validation error forces the render response for our data
470         if (context.getRenderResponse()) {
471             _isValidChilds = false;
472         }
473     }
474 
475     private void process(FacesContext context, UIComponent component, int processAction) {
476         switch (processAction) {
477         case PROCESS_DECODES:
478             component.processDecodes(context);
479             break;
480         case PROCESS_VALIDATORS:
481             component.processValidators(context);
482             break;
483         case PROCESS_UPDATES:
484             component.processUpdates(context);
485             break;
486         }
487     }
488 
489     /**
490      * Called from HtmlDataTable.encodeBegin, ie called once when rendering
491      * for the entire table starts.
492      */
493     public void encodeTableBegin(FacesContext context) {
494         setRowIndex(-1);
495         _initialDescendantComponentState = null;
496         if (_isValidChilds && !hasErrorMessages(context)) {
497             //Refresh DataModel for rendering:
498             _dataModelMap.clear();
499             _cellStates.clear();
500         }
501     }
502 
503     protected boolean hasErrorMessages(FacesContext context) {
504         for (Iterator iter = context.getMessages(); iter.hasNext();) {
505             FacesMessage message = (FacesMessage) iter.next();
506             if (FacesMessage.SEVERITY_ERROR.compareTo(message.getSeverity()) <= 0) {
507                 return true;
508             }
509         }
510         return false;
511     }
512 
513     /**
514      * Called from HtmlDataTable.encodeEnd, ie called once after rendering
515      * for the entire table has completed.
516      */
517     public void encodeTableEnd(FacesContext context) {
518         setRowIndex(-1);
519     }
520 
521     private class EditableValueHolderState {
522         private final Object _value;
523         private final boolean _localValueSet;
524         private final boolean _valid;
525         private final Object _submittedValue;
526 
527         public EditableValueHolderState(EditableValueHolder evh) {
528             _value = evh.getLocalValue();
529             _localValueSet = evh.isLocalValueSet();
530             _valid = evh.isValid();
531             _submittedValue = evh.getSubmittedValue();
532         }
533 
534         public void restoreState(EditableValueHolder evh) {
535             evh.setValue(_value);
536             evh.setLocalValueSet(_localValueSet);
537             evh.setValid(_valid);
538             evh.setSubmittedValue(_submittedValue);
539         }
540     }
541 
542     /// ------ EUR/SI/BS/JC MODIFICATIONS START HERE ---------
543 
544     public String getClientId(FacesContext context) {
545         String clientId2 = super.getClientId(context);
546         int rowIndex = getRowIndex();
547         if (rowIndex == -1) {
548             return clientId2;
549         }
550         else {
551             return clientId2 + "_" + rowIndex;
552         }
553     }
554 
555     /// ------ EUR/SI/BS/JC MODIFICATIONS END HERE ---------
556 }