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 }