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