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 }