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 }