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 }