001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with this 004 * work for additional information regarding copyright ownership. The ASF 005 * licenses this file to you under the Apache License, Version 2.0 (the 006 * "License"); you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations under 015 * the License. 016 * 017 * $Id: UIEditableList.java 28610 2008-01-09 17:13:52Z sfermigier $ 018 */ 019 020package org.nuxeo.ecm.platform.ui.web.component.list; 021 022import java.io.IOException; 023import java.io.Serializable; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.List; 027import java.util.Map; 028 029import javax.el.ELException; 030import javax.el.ValueExpression; 031import javax.faces.FacesException; 032import javax.faces.application.FacesMessage; 033import javax.faces.component.ContextCallback; 034import javax.faces.component.NamingContainer; 035import javax.faces.component.UIComponent; 036import javax.faces.component.UIForm; 037import javax.faces.component.UIInput; 038import javax.faces.component.UpdateModelException; 039import javax.faces.component.visit.VisitCallback; 040import javax.faces.component.visit.VisitContext; 041import javax.faces.component.visit.VisitHint; 042import javax.faces.component.visit.VisitResult; 043import javax.faces.context.FacesContext; 044import javax.faces.event.ExceptionQueuedEvent; 045import javax.faces.event.ExceptionQueuedEventContext; 046import javax.faces.event.FacesEvent; 047import javax.faces.event.PhaseId; 048 049import org.apache.commons.logging.Log; 050import org.apache.commons.logging.LogFactory; 051import org.nuxeo.ecm.core.api.PropertyException; 052import org.nuxeo.ecm.core.api.model.impl.ListProperty; 053import org.nuxeo.ecm.platform.el.FieldAdapterManager; 054import org.nuxeo.ecm.platform.ui.web.component.ResettableComponent; 055import org.nuxeo.ecm.platform.ui.web.model.EditableModel; 056import org.nuxeo.ecm.platform.ui.web.model.ProtectedEditableModel; 057import org.nuxeo.ecm.platform.ui.web.model.impl.EditableModelImpl; 058import org.nuxeo.ecm.platform.ui.web.model.impl.EditableModelRowEvent; 059import org.nuxeo.ecm.platform.ui.web.model.impl.ProtectedEditableModelImpl; 060import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils; 061 062import com.sun.faces.facelets.tag.jsf.ComponentSupport; 063import com.sun.faces.util.MessageFactory; 064 065/** 066 * Editable table component. 067 * <p> 068 * Allows to add/remove elements from an {@link UIEditableList}, inspired from Trinidad UIXCollection component. 069 * 070 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> 071 */ 072public class UIEditableList extends UIInput implements NamingContainer, ResettableComponent { 073 074 public static final String COMPONENT_TYPE = UIEditableList.class.getName(); 075 076 public static final String COMPONENT_FAMILY = UIEditableList.class.getName(); 077 078 private static final Log log = LogFactory.getLog(UIEditableList.class); 079 080 // use this key to indicate uninitialized state. 081 private static final Object _NULL = new Object(); 082 083 protected String model = ""; 084 085 protected Object template; 086 087 protected Boolean diff; 088 089 protected Integer number; 090 091 protected Boolean removeEmpty; 092 093 private InternalState state; 094 095 private static final class InternalState implements Serializable { 096 097 private static final long serialVersionUID = 4730664880938551346L; 098 099 private transient Object _value; 100 101 private transient boolean _isInitialized = false; 102 103 // this is the rowKey used to retrieve the default stamp-state for all 104 // rows: 105 private transient Object _initialStampStateKey = _NULL; 106 107 private EditableModel _model; 108 109 private StampState _stampState; 110 } 111 112 @Override 113 public String getFamily() { 114 return COMPONENT_FAMILY; 115 } 116 117 // state management 118 119 protected final InternalState getInternalState(boolean create) { 120 if (state == null && create) { 121 state = new InternalState(); 122 } 123 return state; 124 } 125 126 protected final StampState getStampState() { 127 InternalState iState = getInternalState(true); 128 if (iState._stampState == null) { 129 iState._stampState = new StampState(); 130 } 131 return iState._stampState; 132 } 133 134 protected final void initializeState(final boolean force) { 135 InternalState iState = getInternalState(true); 136 if (!iState._isInitialized || force) { 137 iState._isInitialized = true; 138 } 139 } 140 141 @Override 142 public Object saveState(FacesContext context) { 143 // _stampState is stored as an instance variable, so it isn't 144 // automatically saved 145 Object superState = super.saveState(context); 146 final Object stampState; 147 final Object editableModel; 148 149 // be careful not to create the internal state too early: 150 // otherwise, the internal state will be shared between 151 // nested table stamps: 152 InternalState iState = getInternalState(false); 153 if (iState != null) { 154 stampState = iState._stampState; 155 editableModel = iState._model; 156 } else { 157 stampState = null; 158 editableModel = null; 159 } 160 161 if (superState != null || stampState != null) { 162 return new Object[] { superState, stampState, getSubmittedValue(), editableModel, model, template, diff, 163 number, removeEmpty }; 164 } 165 return null; 166 } 167 168 @Override 169 public Object getValue() { 170 Object value = super.getValue(); 171 if (value instanceof ListProperty) { 172 try { 173 value = ((ListProperty) value).getValue(); 174 value = FieldAdapterManager.getValueForDisplay(value); 175 } catch (PropertyException e) { 176 } 177 } 178 return value; 179 } 180 181 @Override 182 public void restoreState(FacesContext context, Object state) { 183 final Object superState; 184 final Object stampState; 185 final Object submittedValue; 186 final Object editableModel; 187 Object[] array = (Object[]) state; 188 if (array != null) { 189 superState = array[0]; 190 stampState = array[1]; 191 submittedValue = array[2]; 192 editableModel = array[3]; 193 model = (String) array[4]; 194 template = array[5]; 195 diff = (Boolean) array[6]; 196 number = (Integer) array[7]; 197 removeEmpty = (Boolean) array[8]; 198 } else { 199 superState = null; 200 stampState = null; 201 submittedValue = null; 202 editableModel = null; 203 } 204 super.restoreState(context, superState); 205 setSubmittedValue(submittedValue); 206 207 if (stampState != null || model != null) { 208 InternalState iState = getInternalState(true); 209 iState._stampState = (StampState) stampState; 210 iState._model = (EditableModel) editableModel; 211 } else { 212 // be careful not to force creation of the internal state 213 // too early: 214 InternalState iState = getInternalState(false); 215 if (iState != null) { 216 iState._stampState = (StampState) stampState; 217 iState._model = (EditableModel) editableModel; 218 } 219 } 220 } 221 222 @SuppressWarnings("rawtypes") 223 protected static boolean valueChanged(Object cached, Object current) { 224 boolean changed = false; 225 if (cached == null) { 226 changed = (current != null); 227 } else if (current == null) { 228 changed = true; 229 } else if (cached instanceof Object[] && current instanceof Object[]) { 230 // arrays do not compare ok if reference is different, so match 231 // each element 232 Object[] cachedArray = (Object[]) cached; 233 Object[] currentArray = (Object[]) current; 234 if (cachedArray.length != currentArray.length) { 235 return true; 236 } else { 237 for (int i = 0; i < cachedArray.length; i++) { 238 if (valueChanged(cachedArray[i], currentArray[i])) { 239 return true; 240 } 241 } 242 } 243 } else if (cached instanceof List && current instanceof List) { 244 // arrays do not compare ok if reference is different, so match 245 // each element 246 List cachedList = (List) cached; 247 List currentList = (List) current; 248 if (cachedList.size() != currentList.size()) { 249 return true; 250 } else { 251 for (int i = 0; i < cachedList.size(); i++) { 252 if (valueChanged(cachedList.get(i), currentList.get(i))) { 253 return true; 254 } 255 } 256 } 257 } else if (cached instanceof Map && current instanceof Map) { 258 // arrays do not compare ok if reference is different, so match 259 // each element 260 Map cachedMap = (Map) cached; 261 Map currentMap = (Map) current; 262 if (cachedMap.size() != currentMap.size()) { 263 return true; 264 } else { 265 for (Object key : cachedMap.keySet()) { 266 if (valueChanged(cachedMap.get(key), currentMap.get(key))) { 267 return true; 268 } 269 } 270 } 271 } else { 272 changed = !(cached.equals(current)); 273 } 274 return changed; 275 } 276 277 protected void flushCachedModel() { 278 InternalState iState = getInternalState(true); 279 Object value = getValue(); 280 Object cachedValue = null; 281 if (iState._model != null) { 282 cachedValue = iState._model.getOriginalData(); 283 } 284 if (valueChanged(cachedValue, value)) { 285 iState._value = value; 286 iState._model = createEditableModel(iState._model, value); 287 } 288 } 289 290 /** 291 * Resets the cache model 292 * <p> 293 * Can be useful when re-rendering a list with ajax and not wanting to keep cached values already submitted. 294 * 295 * @since 5.3.1 296 */ 297 @Override 298 public void resetCachedModel() { 299 InternalState iState = getInternalState(true); 300 Object value = getValue(); 301 iState._value = value; 302 iState._model = createEditableModel(iState._model, value); 303 // also reset sub components state 304 iState._stampState = null; 305 } 306 307 /** 308 * Returns the value exposed in request map for the model name. 309 * <p> 310 * This is useful for restoring this value in the request map. 311 * 312 * @since 5.4.2 313 */ 314 protected final Object saveRequestMapModelValue() { 315 String modelName = getModel(); 316 if (modelName != null) { 317 FacesContext context = getFacesContext(); 318 Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); 319 if (requestMap.containsKey(modelName)) { 320 return requestMap.get(modelName); 321 } 322 } 323 return null; 324 } 325 326 /** 327 * Restores the given value in the request map for the model name. 328 * 329 * @since 5.4.2 330 */ 331 protected final void restoreRequestMapModelValue(Object value) { 332 String modelName = getModel(); 333 if (modelName != null) { 334 FacesContext context = getFacesContext(); 335 Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); 336 if (value == null) { 337 Object old = requestMap.remove(modelName); 338 if (log.isDebugEnabled()) { 339 log.debug("Removing exposed model value for " + getId() + ": " + old); 340 } 341 } else { 342 requestMap.put(modelName, value); 343 if (log.isDebugEnabled()) { 344 log.debug("Exposing model value for " + getId() + ": " + value); 345 } 346 } 347 } 348 } 349 350 protected final void exposeRequestMapModelValue(Object oldRequestValue) { 351 Object modelValue; 352 EditableModel model = getEditableModel(); 353 if (model == null || !model.isRowAvailable()) { 354 modelValue = oldRequestValue; 355 } else { 356 modelValue = getProtectedModel(model); 357 } 358 restoreRequestMapModelValue(modelValue); 359 } 360 361 /** 362 * Prepares this component for a change in the rowData. 363 * <p> 364 * This method should be called right before the rowData changes. It saves the internal states of all the stamps of 365 * this component so that they can be restored when the rowData is reverted. 366 */ 367 protected final void preRowDataChange() { 368 // save stamp state 369 StampState stampState = getStampState(); 370 FacesContext context = getFacesContext(); 371 Object currencyObj = getRowKey(); 372 int position = 0; 373 for (UIComponent stamp : getChildren()) { 374 if (stamp.isTransient()) { 375 continue; 376 } 377 Object state = StampState.saveStampState(context, stamp); 378 // String stampId = stamp.getId(); 379 // TODO 380 // temporarily use position. later we need to use ID's to access 381 // stamp state everywhere, and special case NamingContainers: 382 String stampId = String.valueOf(position++); 383 stampState.put(currencyObj, stampId, state); 384 } 385 } 386 387 /** 388 * Sets up this component to use the new rowData. 389 * <p> 390 * This method should be called right after the rowData changes. It sets up the var EL variable to be the current 391 * rowData. It also sets up the internal states of all the stamps of this component to match this new rowData. 392 */ 393 protected final void postRowDataChange(Object oldRequestValue) { 394 StampState stampState = getStampState(); 395 FacesContext context = getFacesContext(); 396 Object currencyObj = getRowKey(); 397 398 // expose model to the request map or remove it given row availability 399 exposeRequestMapModelValue(oldRequestValue); 400 401 int position = 0; 402 for (UIComponent stamp : getChildren()) { 403 if (stamp.isTransient()) { 404 continue; 405 } 406 // String stampId = stamp.getId(); 407 // TODO 408 // temporarily use position. later we need to use ID's to access 409 // stamp state everywhere, and special case NamingContainers: 410 String stampId = String.valueOf(position++); 411 Object state = stampState.get(currencyObj, stampId); 412 if (state == null) { 413 Object iniStateObj = getCurrencyKeyForInitialStampState(); 414 state = stampState.get(iniStateObj, stampId); 415 if (state == null) { 416 // can happen in case model has been reset, see #resetCachedModel 417 if (log.isDebugEnabled()) { 418 log.debug(String.format("Missing stamp state on '%s' for stampId=%s and key=%s", getId(), 419 stampId, iniStateObj)); 420 } 421 continue; 422 } 423 } 424 StampState.restoreStampState(context, stamp, state); 425 } 426 } 427 428 public ProtectedEditableModel getProtectedModel(EditableModel model) { 429 return new ProtectedEditableModelImpl(getId(), model, getParentList(), getValueExpression("value")); 430 } 431 432 // parent list detection, cached 433 434 protected transient UIEditableList parentList; 435 436 protected transient boolean parentListSet = false; 437 438 protected ProtectedEditableModel getParentList() { 439 if (parentList == null && !parentListSet) { 440 UIComponent parent = getParent(); 441 while (parent != null) { 442 if (parent instanceof UIForm) { 443 // don't bother 444 break; 445 } 446 if (parent instanceof UIEditableList) { 447 parentList = (UIEditableList) parent; 448 parentListSet = true; 449 break; 450 } 451 parent = parent.getParent(); 452 } 453 } 454 if (parentList != null) { 455 return parentList.getProtectedModel(parentList.getEditableModel()); 456 } 457 return null; 458 } 459 460 /** 461 * Gets the currencyObject to setup the rowData to use to build initial stamp state. 462 */ 463 protected Object getCurrencyKeyForInitialStampState() { 464 InternalState iState = getInternalState(false); 465 if (iState == null) { 466 return null; 467 } 468 469 Object rowKey = iState._initialStampStateKey; 470 return (rowKey == _NULL) ? null : rowKey; 471 } 472 473 // model management 474 475 /** 476 * Gets the EditableModel to use with this component. 477 */ 478 public final EditableModel getEditableModel() { 479 InternalState iState = getInternalState(true); 480 if (iState._model == null) { 481 initializeState(false); 482 iState._value = getValue(); 483 iState._model = createEditableModel(null, iState._value); 484 assert iState._model != null; 485 } 486 // model might not have been created if createIfNull is false: 487 if ((iState._initialStampStateKey == _NULL) && (iState._model != null)) { 488 // if we have not already initialized the initialStampStateKey 489 // that means that we don't have any stamp-state to use as the 490 // default 491 // state for rows that we have not seen yet. So... 492 // we will use any stamp-state for the initial rowKey on the model 493 // as the default stamp-state for all rows: 494 iState._initialStampStateKey = iState._model.getRowKey(); 495 } 496 return iState._model; 497 } 498 499 /** 500 * Returns a new EditableModel from given value. 501 * 502 * @param current the current CollectionModel, or null if there is none. 503 * @param value this is the value returned from {@link #getValue()} 504 */ 505 protected EditableModel createEditableModel(EditableModel current, Object value) { 506 EditableModel model = new EditableModelImpl(value, getTemplate()); 507 Integer defaultNumber = getNumber(); 508 int missing = 0; 509 if (defaultNumber != null) { 510 missing = defaultNumber - model.size(); 511 } 512 if (defaultNumber != null && missing > 0) { 513 for (int i = 0; i < missing; i++) { 514 model.addTemplateValue(); 515 } 516 } 517 return model; 518 } 519 520 /** 521 * Checks to see if the current row is available. This is useful when the total number of rows is not known. 522 * 523 * @see EditableModel#isRowAvailable 524 * @return true if the current row is available. 525 */ 526 public final boolean isRowAvailable() { 527 return getEditableModel().isRowAvailable(); 528 } 529 530 /** 531 * Checks to see if the current row is modified. 532 * 533 * @see EditableModel#isRowModified 534 * @return true if the current row is modified. 535 */ 536 public final boolean isRowModified() { 537 return getEditableModel().isRowModified(); 538 } 539 540 /** 541 * Gets the total number of rows in this table. 542 * 543 * @see EditableModel#getRowCount 544 * @return -1 if the total number is not known. 545 */ 546 public final int getRowCount() { 547 return getEditableModel().getRowCount(); 548 } 549 550 /** 551 * Gets the index of the current row. 552 * 553 * @see EditableModel#getRowIndex 554 * @return -1 if the current row is unavailable. 555 */ 556 public final int getRowIndex() { 557 return getEditableModel().getRowIndex(); 558 } 559 560 /** 561 * Gets the rowKey of the current row. 562 * 563 * @see EditableModel#getRowKey 564 * @return null if the current row is unavailable. 565 */ 566 public final Integer getRowKey() { 567 return getEditableModel().getRowKey(); 568 } 569 570 /** 571 * Gets the data for the current row. 572 * 573 * @see EditableModel#getRowData 574 * @return null if the current row is unavailable 575 */ 576 public final Object getRowData() { 577 EditableModel model = getEditableModel(); 578 // we need to call isRowAvailable() here because the 1.0 sun RI was 579 // throwing exceptions when getRowData() was called with rowIndex=-1 580 return model.isRowAvailable() ? model.getRowData() : null; 581 } 582 583 /** 584 * Makes a row current. 585 * <p> 586 * This method calls {@link #preRowDataChange} and {@link #postRowDataChange} as appropriate. 587 * 588 * @see EditableModel#setRowIndex 589 * @param rowIndex The rowIndex of the row that should be made current. Use -1 to clear the current row. 590 */ 591 public void setRowIndex(int rowIndex) { 592 // do not change row index if not needed 593 int current = getEditableModel().getRowIndex(); 594 if (current != rowIndex) { 595 Object oldValue = saveRequestMapModelValue(); 596 preRowDataChange(); 597 getEditableModel().setRowIndex(rowIndex); 598 postRowDataChange(oldValue); 599 } else { 600 Object oldRequestValue = saveRequestMapModelValue(); 601 exposeRequestMapModelValue(oldRequestValue); 602 } 603 } 604 605 /** 606 * Makes a row current. 607 * <p> 608 * This method calls {@link #preRowDataChange} and {@link #postRowDataChange} as appropriate. 609 * 610 * @see EditableModel#setRowKey 611 * @param rowKey The rowKey of the row that should be made current. Use null to clear the current row. 612 */ 613 public void setRowKey(Integer rowKey) { 614 // do not change row key if not needed 615 Integer current = getEditableModel().getRowKey(); 616 if ((current != null && !current.equals(rowKey)) || (current == null && rowKey != null)) { 617 // XXX AT: do not save state before setting row key as current index 618 // may not point to the same object anymore (XXX: need to handle this 619 // better, as events may change the data too, in which case we would 620 // want the state to be saved). 621 // preRowDataChange(); 622 Object oldRequestValue = saveRequestMapModelValue(); 623 getEditableModel().setRowKey(rowKey); 624 postRowDataChange(oldRequestValue); 625 } else { 626 Object oldRequestValue = saveRequestMapModelValue(); 627 exposeRequestMapModelValue(oldRequestValue); 628 } 629 } 630 631 /** 632 * Records a value modification. 633 * 634 * @see EditableModel#recordValueModified 635 */ 636 public final void recordValueModified(int index, Object newValue) { 637 getEditableModel().recordValueModified(index, newValue); 638 } 639 640 /** 641 * Adds a value to the end of the editable model. 642 * 643 * @param value the value to add 644 * @return true if value was added. 645 */ 646 public boolean addValue(Object value) { 647 return getEditableModel().addValue(value); 648 } 649 650 /** 651 * Inserts value at given index on the editable model. 652 * 653 * @throws IllegalArgumentException if model does not handle this index. 654 */ 655 public void insertValue(int index, Object value) { 656 getEditableModel().insertValue(index, value); 657 } 658 659 /** 660 * Modifies value at given index on the editable model. 661 * 662 * @return the old value at that index. 663 * @throws IllegalArgumentException if model does not handle one of given indexes. 664 */ 665 public Object moveValue(int fromIndex, int toIndex) { 666 return getEditableModel().moveValue(fromIndex, toIndex); 667 } 668 669 /** 670 * Removes value at given index on the editable model. 671 * 672 * @return the old value at that index. 673 * @throws IllegalArgumentException if model does not handle this index. 674 */ 675 public Object removeValue(int index) { 676 int oldIndex = getRowIndex(); 677 setRowIndex(index); 678 EditableModel model = getEditableModel(); 679 Integer rowKey = model.getRowKey(); 680 if (rowKey != null) { 681 InternalState iState = getInternalState(false); 682 if (iState != null) { 683 iState._stampState.clearIndex(rowKey.intValue()); 684 } 685 } 686 Object res = model.removeValue(index); 687 setRowIndex(oldIndex); 688 return res; 689 } 690 691 /** 692 * Gets model name exposed in request map. 693 */ 694 public String getModel() { 695 if (model != null) { 696 return model; 697 } 698 ValueExpression ve = getValueExpression("model"); 699 if (ve != null) { 700 try { 701 return (String) ve.getValue(getFacesContext().getELContext()); 702 } catch (ELException e) { 703 throw new FacesException(e); 704 } 705 } else { 706 return null; 707 } 708 } 709 710 /** 711 * Sets model name exposed in request map. 712 */ 713 public void setModel(String model) { 714 this.model = model; 715 } 716 717 /** 718 * Gets template to be used when adding new values to the model. 719 */ 720 public Object getTemplate() { 721 if (template != null) { 722 return template; 723 } 724 ValueExpression ve = getValueExpression("template"); 725 if (ve != null) { 726 try { 727 Object res = ve.getValue(getFacesContext().getELContext()); 728 if (res instanceof String) { 729 // try to resolve a second time in case it's an expression 730 res = ComponentTagUtils.resolveElExpression(getFacesContext(), (String) res); 731 } 732 return res; 733 } catch (ELException e) { 734 throw new FacesException(e); 735 } 736 } else { 737 return null; 738 } 739 } 740 741 /** 742 * Sets template to be used when adding new values to the model. 743 */ 744 public final void setTemplate(Object template) { 745 this.template = template; 746 } 747 748 /** 749 * Gets boolean stating if diff must be used when saving the value submitted. 750 */ 751 public Boolean getDiff() { 752 if (diff != null) { 753 return diff; 754 } 755 ValueExpression ve = getValueExpression("diff"); 756 if (ve != null) { 757 try { 758 return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); 759 } catch (ELException e) { 760 throw new FacesException(e); 761 } 762 } else { 763 // default value 764 return false; 765 } 766 } 767 768 /** 769 * Sets boolean stating if diff must be used when saving the value submitted. 770 */ 771 public void setDiff(Boolean diff) { 772 this.diff = diff; 773 } 774 775 public Integer getNumber() { 776 if (number != null) { 777 return number; 778 } 779 ValueExpression ve = getValueExpression("number"); 780 if (ve != null) { 781 try { 782 return ((Number) ve.getValue(getFacesContext().getELContext())).intValue(); 783 } catch (ELException e) { 784 throw new FacesException(e); 785 } 786 } else { 787 // default value 788 return null; 789 } 790 } 791 792 public void setNumber(Integer number) { 793 this.number = number; 794 } 795 796 public Boolean getRemoveEmpty() { 797 if (removeEmpty != null) { 798 return removeEmpty; 799 } 800 ValueExpression ve = getValueExpression("removeEmpty"); 801 if (ve != null) { 802 try { 803 return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); 804 } catch (ELException e) { 805 throw new FacesException(e); 806 } 807 } else { 808 // default value 809 return false; 810 } 811 } 812 813 public void setRemoveEmpty(Boolean removeEmpty) { 814 this.removeEmpty = removeEmpty; 815 } 816 817 /** 818 * Override container client id resolution to handle recursion. 819 */ 820 @Override 821 @SuppressWarnings("deprecation") 822 public String getContainerClientId(FacesContext context) { 823 String id = super.getClientId(context); 824 // avoid trigger of editable model creation for id retrieval 825 InternalState iState = getInternalState(false); 826 if (iState == null || iState._model == null) { 827 return id; 828 } 829 int index = getRowIndex(); 830 if (index != -1) { 831 id += NamingContainer.SEPARATOR_CHAR + String.valueOf(index); 832 } 833 return id; 834 } 835 836 @Override 837 public String getRendererType() { 838 return null; 839 } 840 841 @Override 842 public void setRendererType(String rendererType) { 843 // do nothing 844 } 845 846 @Override 847 public final void encodeBegin(FacesContext context) throws IOException { 848 initializeState(false); 849 flushCachedModel(); 850 851 super.encodeBegin(context); 852 } 853 854 @Override 855 public void encodeEnd(FacesContext context) throws IOException { 856 super.encodeEnd(context); 857 } 858 859 @Override 860 public boolean getRendersChildren() { 861 return true; 862 } 863 864 /** 865 * Repeatedly render the children as many times as needed. 866 */ 867 @Override 868 public void encodeChildren(final FacesContext context) throws IOException { 869 if (!isRendered()) { 870 return; 871 } 872 873 processFacetsAndChildren(context, PhaseId.RENDER_RESPONSE); 874 encodeTemplate(context); 875 } 876 877 protected void encodeTemplate(FacesContext context) throws IOException { 878 // helper method to be overriden by UIJavascriptList 879 } 880 881 // events 882 /** 883 * Delivers a wrapped event to the appropriate component. If the event is a special wrapped event, it is unwrapped. 884 * 885 * @param event a FacesEvent 886 * @throws javax.faces.event.AbortProcessingException 887 */ 888 @Override 889 public void broadcast(FacesEvent event) { 890 if (event instanceof EditableModelRowEvent) { 891 EditableModelRowEvent rowEvent = (EditableModelRowEvent) event; 892 Integer old = getRowKey(); 893 Object requestMapValue = saveRequestMapModelValue(); 894 try { 895 FacesContext context = FacesContext.getCurrentInstance(); 896 setRowKey(rowEvent.getKey()); 897 UIComponent source = rowEvent.getComponent(); 898 FacesEvent wrapped = rowEvent.getEvent(); 899 UIComponent compositeParent = null; 900 try { 901 if (!UIComponent.isCompositeComponent(source)) { 902 compositeParent = UIComponent.getCompositeComponentParent(source); 903 } 904 if (compositeParent != null) { 905 compositeParent.pushComponentToEL(context, null); 906 } 907 source.pushComponentToEL(context, null); 908 wrapped.getComponent().broadcast(wrapped); 909 } finally { 910 source.popComponentFromEL(context); 911 if (compositeParent != null) { 912 compositeParent.popComponentFromEL(context); 913 } 914 } 915 setRowKey(old); 916 } finally { 917 restoreRequestMapModelValue(requestMapValue); 918 } 919 } else { 920 super.broadcast(event); 921 } 922 } 923 924 /** 925 * Queues an event. If there is a currency set on this table, then the event will be wrapped so that when it is 926 * finally delivered, the correct currency will be restored. 927 * 928 * @param event a FacesEvent 929 */ 930 @Override 931 public void queueEvent(FacesEvent event) { 932 // we want to wrap up the event so we can execute it in the correct 933 // context (with the correct rowKey/rowData): 934 Integer currencyKey = getRowKey(); 935 if (currencyKey == null) { 936 // not a row event 937 super.queueEvent(event); 938 } else { 939 super.queueEvent(new EditableModelRowEvent(this, event, currencyKey)); 940 } 941 } 942 943 private boolean requiresRowIteration(VisitContext ctx) { 944 return !ctx.getHints().contains(VisitHint.SKIP_ITERATION); 945 } 946 947 private boolean visitRows(VisitContext context, VisitCallback callback, boolean visitRows) { 948 // Iterate over our children, once per row 949 int processed = 0; 950 int oldIndex = getRowIndex(); 951 int rowIndex = getRowIndex(); 952 boolean rowChanged = false; 953 int rows = 0; 954 if (visitRows) { 955 rowIndex = -1; 956 rows = getRowCount(); 957 } 958 959 Object requestMapValue = saveRequestMapModelValue(); 960 try { 961 962 while (true) { 963 964 if (visitRows) { 965 if ((rows > 0) && (++processed > rows)) { 966 break; 967 } 968 // Expose the current row in the specified 969 // request attribute 970 setRowIndex(++rowIndex); 971 rowChanged = true; 972 if (!isRowAvailable()) { 973 break; // Scrolled past the last row 974 } 975 } 976 977 // Visit as required on the *children* 978 if (getChildCount() > 0) { 979 for (UIComponent kid : getChildren()) { 980 if (kid.getChildCount() > 0) { 981 for (UIComponent grandkid : kid.getChildren()) { 982 if (grandkid.visitTree(context, callback)) { 983 return true; 984 } 985 } 986 } 987 } 988 } 989 990 if (!visitRows) { 991 break; 992 } 993 994 } 995 } finally { 996 if (rowChanged) { 997 setRowIndex(oldIndex); 998 restoreRequestMapModelValue(requestMapValue); 999 } 1000 } 1001 1002 return false; 1003 } 1004 1005 private boolean doVisitChildren(VisitContext context, boolean visitRows) { 1006 1007 // Just need to check whether there are any ids under this 1008 // subtree. Make sure row index is cleared out since 1009 // getSubtreeIdsToVisit() needs our row-less client id. 1010 if (visitRows) { 1011 setRowIndex(-1); 1012 } 1013 Collection<String> idsToVisit = context.getSubtreeIdsToVisit(this); 1014 assert idsToVisit != null; 1015 1016 // All ids or non-empty collection means we need to visit our children. 1017 return (!idsToVisit.isEmpty()); 1018 } 1019 1020 /** 1021 * Rough adapt of the UI data behaviour. 1022 */ 1023 @Override 1024 public boolean visitTree(VisitContext context, VisitCallback callback) { 1025 if (!isVisitable(context)) { 1026 return false; 1027 } 1028 1029 FacesContext facesContext = context.getFacesContext(); 1030 boolean visitRows = requiresRowIteration(context); 1031 1032 int oldRowIndex = -1; 1033 Object requestMapValue = null; 1034 if (visitRows) { 1035 oldRowIndex = getRowIndex(); 1036 if (oldRowIndex != -1) { 1037 setRowIndex(-1); 1038 requestMapValue = saveRequestMapModelValue(); 1039 } 1040 } 1041 1042 pushComponentToEL(facesContext, null); 1043 1044 try { 1045 1046 VisitResult result = context.invokeVisitCallback(this, callback); 1047 if (result == VisitResult.COMPLETE) { 1048 return true; 1049 } 1050 if ((result == VisitResult.ACCEPT) && doVisitChildren(context, visitRows)) { 1051 if (visitRows(context, callback, visitRows)) { 1052 return true; 1053 } 1054 } 1055 } finally { 1056 popComponentFromEL(facesContext); 1057 if (visitRows && oldRowIndex != -1) { 1058 setRowIndex(oldRowIndex); 1059 restoreRequestMapModelValue(requestMapValue); 1060 } 1061 } 1062 1063 // return false to allow the visit to continue 1064 return false; 1065 } 1066 1067 @Override 1068 public void processDecodes(FacesContext context) { 1069 if (!isRendered()) { 1070 return; 1071 } 1072 1073 initializeState(false); 1074 1075 flushCachedModel(); 1076 1077 // process this component decode before so that it can help detecting new/deleted rows before applying children 1078 // decodes 1079 decode(context); 1080 1081 pushComponentToEL(context, this); 1082 processFacetsAndChildren(context, PhaseId.APPLY_REQUEST_VALUES); 1083 popComponentFromEL(context); 1084 1085 if (isImmediate()) { 1086 executeValidate(context); 1087 } 1088 } 1089 1090 @Override 1091 public void processValidators(FacesContext context) { 1092 if (!isRendered()) { 1093 return; 1094 } 1095 1096 initializeState(true); 1097 1098 if (!isImmediate()) { 1099 executeValidate(context); 1100 } 1101 } 1102 1103 @Override 1104 public void processUpdates(FacesContext context) { 1105 if (!isRendered()) { 1106 return; 1107 } 1108 1109 initializeState(true); 1110 1111 try { 1112 updateModel(context); 1113 } catch (RuntimeException e) { 1114 context.renderResponse(); 1115 throw e; 1116 } 1117 1118 if (!isValid()) { 1119 context.renderResponse(); 1120 } else { 1121 // force reset 1122 resetCachedModel(); 1123 } 1124 } 1125 1126 /** 1127 * Overridden to handle diff boolean value, see NXP-16515. 1128 */ 1129 public void updateModel(FacesContext context) { 1130 1131 if (context == null) { 1132 throw new NullPointerException(); 1133 } 1134 1135 if (!isValid() || !isLocalValueSet()) { 1136 return; 1137 } 1138 ValueExpression ve = getValueExpression("value"); 1139 if (ve != null) { 1140 Throwable caught = null; 1141 FacesMessage message = null; 1142 try { 1143 Boolean setDiff = getDiff(); 1144 if (setDiff) { 1145 // set list diff instead of the whole list 1146 // FIXME NXP-16515 1147 // EditableModel model = getEditableModel(); 1148 // ve.setValue(context.getELContext(), model.getListDiff()); 1149 ve.setValue(context.getELContext(), getLocalValue()); 1150 } else { 1151 ve.setValue(context.getELContext(), getLocalValue()); 1152 } 1153 setValue(null); 1154 setLocalValueSet(false); 1155 } catch (ELException e) { 1156 caught = e; 1157 String messageStr = e.getMessage(); 1158 Throwable result = e.getCause(); 1159 while (null != result && result.getClass().isAssignableFrom(ELException.class)) { 1160 messageStr = result.getMessage(); 1161 result = result.getCause(); 1162 } 1163 if (null == messageStr) { 1164 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, 1165 MessageFactory.getLabel(context, this)); 1166 } else { 1167 message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 1168 } 1169 setValid(false); 1170 } catch (Exception e) { 1171 caught = e; 1172 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); 1173 setValid(false); 1174 } 1175 if (caught != null) { 1176 assert message != null; 1177 // PENDING(edburns): verify this is in the spec. 1178 UpdateModelException toQueue = new UpdateModelException(message, caught); 1179 ExceptionQueuedEventContext eventContext = new ExceptionQueuedEventContext(context, toQueue, this, 1180 PhaseId.UPDATE_MODEL_VALUES); 1181 context.getApplication().publishEvent(context, ExceptionQueuedEvent.class, eventContext); 1182 1183 } 1184 1185 } 1186 } 1187 1188 protected void processFacetsAndChildren(final FacesContext context, final PhaseId phaseId) { 1189 List<UIComponent> stamps = getChildren(); 1190 int oldIndex = getRowIndex(); 1191 boolean rowChanged = false; 1192 int end = getRowCount(); 1193 Object requestMapValue = saveRequestMapModelValue(); 1194 try { 1195 int first = 0; 1196 for (int i = first; i < end; i++) { 1197 rowChanged = true; 1198 setRowIndex(i); 1199 if (isRowAvailable()) { 1200 for (UIComponent stamp : stamps) { 1201 processComponent(context, stamp, phaseId); 1202 } 1203 if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 1204 // detect changes during process update phase and fill 1205 // the EditableModel list diff. 1206 // XXX maybe handle new values with js list too 1207 if (isRowModified()) { 1208 recordValueModified(i, getRowData()); 1209 } 1210 } 1211 } else { 1212 break; 1213 } 1214 } 1215 } finally { 1216 if (rowChanged) { 1217 setRowIndex(oldIndex); 1218 restoreRequestMapModelValue(requestMapValue); 1219 } 1220 } 1221 } 1222 1223 protected final void processComponent(FacesContext context, UIComponent component, PhaseId phaseId) { 1224 if (component != null) { 1225 if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { 1226 component.processDecodes(context); 1227 } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) { 1228 component.processValidators(context); 1229 } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 1230 component.processUpdates(context); 1231 } else if (phaseId == PhaseId.RENDER_RESPONSE) { 1232 try { 1233 ComponentSupport.encodeRecursive(context, component); 1234 } catch (IOException err) { 1235 log.error("Error while rendering component " + component); 1236 } 1237 } else { 1238 throw new IllegalArgumentException("Bad PhaseId:" + phaseId); 1239 } 1240 } 1241 } 1242 1243 @SuppressWarnings("rawtypes") 1244 private void executeValidate(FacesContext context) { 1245 try { 1246 pushComponentToEL(context, this); 1247 processFacetsAndChildren(context, PhaseId.PROCESS_VALIDATIONS); 1248 // process updates right away so that list can perform its global validation 1249 processFacetsAndChildren(context, PhaseId.UPDATE_MODEL_VALUES); 1250 popComponentFromEL(context); 1251 1252 EditableModel model = getEditableModel(); 1253 if (model.isDirty()) { 1254 // remove empty values if needed 1255 Boolean removeEmpty = getRemoveEmpty(); 1256 Object data = model.getWrappedData(); 1257 if (removeEmpty && data instanceof List) { 1258 Object template = getTemplate(); 1259 List dataList = (List) data; 1260 for (int i = dataList.size() - 1; i > -1; i--) { 1261 Object item = dataList.get(i); 1262 if (item == null || item.equals(template)) { 1263 model.removeValue(i); 1264 } 1265 } 1266 } 1267 } 1268 1269 Object submitted = model.getWrappedData(); 1270 if (submitted == null) { 1271 // set submitted to empty list to force validation 1272 submitted = Collections.emptyList(); 1273 } 1274 setSubmittedValue(submitted); 1275 validate(context); 1276 1277 } catch (RuntimeException e) { 1278 context.renderResponse(); 1279 throw e; 1280 } 1281 1282 if (!isValid()) { 1283 context.renderResponse(); 1284 } 1285 } 1286 1287 @Override 1288 public boolean invokeOnComponent(FacesContext context, String clientId, ContextCallback callback) 1289 throws FacesException { 1290 if (null == context || null == clientId || null == callback) { 1291 throw new NullPointerException(); 1292 } 1293 1294 // try invoking on list 1295 String myId = super.getClientId(context); 1296 if (clientId.equals(myId)) { 1297 try { 1298 pushComponentToEL(context, UIComponent.getCompositeComponentParent(this)); 1299 callback.invokeContextCallback(context, this); 1300 return true; 1301 } catch (RuntimeException e) { 1302 // TODO what is caught here? 1303 throw new FacesException(e); 1304 } finally { 1305 popComponentFromEL(context); 1306 } 1307 } 1308 1309 List<UIComponent> stamps = getChildren(); 1310 int oldIndex = getRowIndex(); 1311 int end = getRowCount(); 1312 boolean rowChanged = false; 1313 boolean found = false; 1314 Object requestMapValue = saveRequestMapModelValue(); 1315 try { 1316 int first = 0; 1317 for (int i = first; i < end; i++) { 1318 rowChanged = true; 1319 setRowIndex(i); 1320 if (isRowAvailable()) { 1321 for (UIComponent stamp : stamps) { 1322 found = stamp.invokeOnComponent(context, clientId, callback); 1323 } 1324 } else { 1325 break; 1326 } 1327 } 1328 } finally { 1329 if (rowChanged) { 1330 setRowIndex(oldIndex); 1331 restoreRequestMapModelValue(requestMapValue); 1332 } 1333 } 1334 1335 return found; 1336 } 1337 1338}