001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package org.apache.myfaces.tobago.component;
021    
022    import javax.faces.application.FacesMessage;
023    import javax.faces.component.EditableValueHolder;
024    import javax.faces.component.NamingContainer;
025    import javax.faces.component.UIComponent;
026    import javax.faces.context.FacesContext;
027    import javax.faces.el.ValueBinding;
028    import javax.faces.model.ArrayDataModel;
029    import javax.faces.model.DataModel;
030    import javax.faces.model.ListDataModel;
031    import javax.faces.model.ResultDataModel;
032    import javax.faces.model.ResultSetDataModel;
033    import javax.faces.model.ScalarDataModel;
034    import javax.servlet.jsp.jstl.sql.Result;
035    import java.io.IOException;
036    import java.sql.ResultSet;
037    import java.util.ArrayList;
038    import java.util.Collection;
039    import java.util.HashMap;
040    import java.util.Iterator;
041    import java.util.List;
042    import java.util.Map;
043    
044    /**
045     * This component is an alternative to its parent.
046     * It was written as an workaround for problems with Sun RI 1.1_02.
047     * See bug TOBAGO-931 in the bug tracker.
048     * To use it, define it in you faces-config.xml.
049     */
050    public class UIDataFixTobago931 extends org.apache.myfaces.tobago.component.UIData {
051    
052      private static final Class OBJECT_ARRAY_CLASS = (new Object[0]).getClass();
053    
054      private int rowIndex = -1;
055      private final Map dataModelMap = new HashMap();
056    
057      // Holds for each row the states of the child components of this UIData.
058      // Note that only "partial" component state is saved: the component fields
059      // that are expected to vary between rows.
060      private final Map rowStates = new HashMap();
061      private Object initialDescendantComponentState = null;
062      private boolean isValidChilds = true;
063    
064      public String getClientId(FacesContext facesContext) {
065        String clientId = super.getClientId(facesContext);
066        if (getRowIndex() >= 0) {
067          return (clientId + NamingContainer.SEPARATOR_CHAR + getRowIndex());
068        } else {
069          return clientId;
070        }
071      }
072    
073      public void processValidators(FacesContext context) {
074        super.processValidators(context);
075        // check if an validation error forces the render response for our data
076        if (context.getRenderResponse()) {
077          isValidChilds = false;
078        }
079      }
080    
081      public void processUpdates(FacesContext context) {
082        super.processUpdates(context);
083        if (context.getRenderResponse()) {
084          isValidChilds = false;
085        }
086      }
087    
088      public void setValue(Object value) {
089        super.setValue(value);
090        dataModelMap.clear();
091        rowStates.clear();
092        isValidChilds = true;
093      }
094    
095      public void setValueBinding(String name, ValueBinding binding) {
096        if (name == null) {
097          throw new NullPointerException("name");
098        } else if (name.equals("value")) {
099          dataModelMap.clear();
100        } else if (name.equals("var") || name.equals("rowIndex")) {
101          throw new IllegalArgumentException("You can never set the 'rowIndex' or the 'var' attribute as a value-binding. "
102              + "Set the property directly instead. Name " + name);
103        }
104        super.setValueBinding(name, binding);
105      }
106    
107      /**
108       * Perform necessary actions when rendering of this component starts,
109       * before delegating to the inherited implementation which calls the
110       * associated renderer's encodeBegin method.
111       */
112      public void encodeBegin(FacesContext context) throws IOException {
113        initialDescendantComponentState = null;
114        if (isValidChilds && !hasErrorMessages(context)) {
115          // Clear the data model so that when rendering code calls
116          // getDataModel a fresh model is fetched from the backing
117          // bean via the value-binding.
118          dataModelMap.clear();
119    
120          // When the data model is cleared it is also necessary to
121          // clear the saved row state, as there is an implicit 1:1
122          // relation between objects in the rowStates and the
123          // corresponding DataModel element.
124          rowStates.clear();
125        }
126        super.encodeBegin(context);
127      }
128    
129      private boolean hasErrorMessages(FacesContext context) {
130        for (Iterator iter = context.getMessages(); iter.hasNext();) {
131          FacesMessage message = (FacesMessage) iter.next();
132          if (FacesMessage.SEVERITY_ERROR.compareTo(message.getSeverity()) <= 0) {
133            return true;
134          }
135        }
136        return false;
137      }
138    
139      public boolean isRowAvailable() {
140        return getDataModel().isRowAvailable();
141      }
142    
143      public int getRowCount() {
144        return getDataModel().getRowCount();
145      }
146    
147      public Object getRowData() {
148        return getDataModel().getRowData();
149      }
150    
151      public int getRowIndex() {
152        return rowIndex;
153      }
154    
155      public void setRowIndex(int rowIndex) {
156        if (rowIndex < -1) {
157          throw new IllegalArgumentException("rowIndex is less than -1");
158        }
159    
160        if (this.rowIndex == rowIndex) {
161          return;
162        }
163    
164        FacesContext facesContext = getFacesContext();
165    
166        if (this.rowIndex == -1) {
167          if (initialDescendantComponentState == null) {
168            // Create a template that can be used to initialise any row
169            // that we haven't visited before, ie a "saved state" that can
170            // be pushed to the "restoreState" method of all the child
171            // components to set them up to represent a clean row.
172            initialDescendantComponentState = saveDescendantComponentStates(getChildren().iterator(), false);
173          }
174        } else {
175          // We are currently positioned on some row, and are about to
176          // move off it, so save the (partial) state of the components
177          // representing the current row. Later if this row is revisited
178          // then we can restore this state.
179          rowStates.put(getClientId(facesContext), saveDescendantComponentStates(getChildren().iterator(), false));
180        }
181    
182        this.rowIndex = rowIndex;
183    
184        DataModel dataModel = getDataModel();
185        dataModel.setRowIndex(rowIndex);
186    
187        String var = getVar();
188        if (rowIndex == -1) {
189          if (var != null) {
190            facesContext.getExternalContext().getRequestMap().remove(var);
191          }
192        } else {
193          if (var != null) {
194            if (isRowAvailable()) {
195              Object rowData = dataModel.getRowData();
196              facesContext.getExternalContext().getRequestMap().put(var, rowData);
197            } else {
198              facesContext.getExternalContext().getRequestMap().remove(var);
199            }
200          }
201        }
202    
203        if (this.rowIndex == -1) {
204          // reset components to initial state
205          restoreDescendantComponentStates(getChildren().iterator(), initialDescendantComponentState, false);
206        } else {
207          Object rowState = rowStates.get(getClientId(facesContext));
208          if (rowState == null) {
209            // We haven't been positioned on this row before, so just
210            // configure the child components of this component with
211            // the standard "initial" state
212            restoreDescendantComponentStates(getChildren().iterator(), initialDescendantComponentState, false);
213          } else {
214            // We have been positioned on this row before, so configure
215            // the child components of this component with the (partial)
216            // state that was previously saved. Fields not in the
217            // partial saved state are left with their original values.
218            restoreDescendantComponentStates(getChildren().iterator(), rowState, false);
219          }
220        }
221      }
222    
223      /**
224       * Overwrite the state of the child components of this component
225       * with data previously saved by method saveDescendantComponentStates.
226       * <p/>
227       * The saved state info only covers those fields that are expected to
228       * vary between rows of a table. Other fields are not modified.
229       */
230      private void restoreDescendantComponentStates(Iterator childIterator, Object state, boolean restoreChildFacets) {
231        Iterator descendantStateIterator = null;
232        while (childIterator.hasNext()) {
233          if (descendantStateIterator == null && state != null) {
234            descendantStateIterator = ((Collection) state).iterator();
235          }
236          UIComponent component = (UIComponent) childIterator.next();
237    
238          // reset the client id (see spec 3.1.6)
239          component.setId(component.getId());
240          if (!component.isTransient()) {
241            Object childState = null;
242            Object descendantState = null;
243            if (descendantStateIterator != null && descendantStateIterator.hasNext()) {
244              Object[] object = (Object[]) descendantStateIterator.next();
245              childState = object[0];
246              descendantState = object[1];
247            }
248            if (component instanceof EditableValueHolder) {
249              ((EditableValueHolderState) childState).restoreState((EditableValueHolder) component);
250            }
251            Iterator childsIterator;
252            if (restoreChildFacets) {
253              childsIterator = component.getFacetsAndChildren();
254            } else {
255              childsIterator = component.getChildren().iterator();
256            }
257            restoreDescendantComponentStates(childsIterator, descendantState, true);
258          }
259        }
260      }
261    
262      /**
263       * Walk the tree of child components of this UIData, saving the parts of
264       * their state that can vary between rows.
265       * <p/>
266       * This is very similar to the process that occurs for normal components
267       * when the view is serialized. Transient components are skipped (no
268       * state is saved for them).
269       * <p/>
270       * If there are no children then null is returned. If there are one or
271       * more children, and all children are transient then an empty collection
272       * is returned; this will happen whenever a table contains only read-only
273       * components.
274       * <p/>
275       * Otherwise a collection is returned which contains an object for every
276       * non-transient child component; that object may itself contain a collection
277       * of the state of that child's child components.
278       */
279      private Object saveDescendantComponentStates(Iterator childIterator, boolean saveChildFacets) {
280        Collection childStates = null;
281        while (childIterator.hasNext()) {
282          if (childStates == null) {
283            childStates = new ArrayList();
284          }
285          UIComponent child = (UIComponent) childIterator.next();
286          if (!child.isTransient()) {
287            // Add an entry to the collection, being an array of two
288            // elements. The first element is the state of the children
289            // of this component; the second is the state of the current
290            // child itself.
291    
292            Iterator childsIterator;
293            if (saveChildFacets) {
294              childsIterator = child.getFacetsAndChildren();
295            } else {
296              childsIterator = child.getChildren().iterator();
297            }
298            Object descendantState = saveDescendantComponentStates(childsIterator, true);
299            Object state = null;
300            if (child instanceof EditableValueHolder) {
301              state = new EditableValueHolderState((EditableValueHolder) child);
302            }
303            childStates.add(new Object[]{state, descendantState});
304          }
305        }
306        return childStates;
307      }
308    
309      private class EditableValueHolderState {
310        private final Object value;
311        private final boolean localValueSet;
312        private final boolean valid;
313        private final Object submittedValue;
314    
315        public EditableValueHolderState(EditableValueHolder evh) {
316          value = evh.getLocalValue();
317          localValueSet = evh.isLocalValueSet();
318          valid = evh.isValid();
319          submittedValue = evh.getSubmittedValue();
320        }
321    
322        public void restoreState(EditableValueHolder evh) {
323          evh.setValue(value);
324          evh.setLocalValueSet(localValueSet);
325          evh.setValid(valid);
326          evh.setSubmittedValue(submittedValue);
327        }
328      }
329    
330      /**
331       * Return the datamodel for this table, potentially fetching the data from
332       * a backing bean via a value-binding if this is the first time this method
333       * has been called.
334       * <p/>
335       * This is complicated by the fact that this table may be nested within
336       * another table. In this case a different datamodel should be fetched
337       * for each row. When nested within a parent table, the parent reference
338       * won't change but parent.getClientId() will, as the suffix changes
339       * depending upon the current row index. A map object on this component
340       * is therefore used to cache the datamodel for each row of the table.
341       * In the normal case where this table is not nested inside a component
342       * that changes its id (like a table does) then this map only ever has
343       * one entry.
344       */
345      private DataModel getDataModel() {
346        DataModel dataModel = null;
347        String clientID = "";
348    
349        UIComponent parent = getParent();
350        if (parent != null) {
351          clientID = parent.getClientId(getFacesContext());
352        }
353        dataModel = (DataModel) dataModelMap.get(clientID);
354        if (dataModel == null) {
355          dataModel = createDataModel();
356          dataModelMap.put(clientID, dataModel);
357        }
358        return dataModel;
359      }
360    
361      /**
362       * Evaluate this object's value property and convert the result into a
363       * DataModel. Normally this object's value property will be a value-binding
364       * which will cause the value to be fetched from some backing bean.
365       * <p/>
366       * The result of fetching the value may be a DataModel object, in which
367       * case that object is returned directly. If the value is of type
368       * List, Array, ResultSet, Result, other object or null then an appropriate
369       * wrapper is created and returned.
370       * <p/>
371       * Null is never returned by this method.
372       */
373      private DataModel createDataModel() {
374        Object value = getValue();
375        if (value == null) {
376          return EMPTY_DATA_MODEL;
377        } else if (value instanceof DataModel) {
378          return (DataModel) value;
379        } else if (value instanceof List) {
380          return new ListDataModel((List) value);
381        } else if (OBJECT_ARRAY_CLASS.isAssignableFrom(value.getClass())) {
382          return new ArrayDataModel((Object[]) value);
383        } else if (value instanceof ResultSet) {
384          return new ResultSetDataModel((ResultSet) value);
385        } else if (value instanceof Result) {
386          return new ResultDataModel((Result) value);
387        } else {
388          return new ScalarDataModel(value);
389        }
390      }
391    
392      private static final DataModel EMPTY_DATA_MODEL = new DataModel() {
393        public boolean isRowAvailable() {
394          return false;
395        }
396    
397        public int getRowCount() {
398          return 0;
399        }
400    
401        public Object getRowData() {
402          throw new IllegalArgumentException();
403        }
404    
405        public int getRowIndex() {
406          return -1;
407        }
408    
409        public void setRowIndex(int i) {
410          if (i < -1) {
411            throw new IllegalArgumentException();
412          }
413        }
414    
415        public Object getWrappedData() {
416          return null;
417        }
418    
419        public void setWrappedData(Object obj) {
420          if (obj == null) {
421            return; //Clearing is allowed
422          }
423          throw new UnsupportedOperationException(this.getClass().getName() + " UnsupportedOperationException");
424        }
425      };
426    }