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