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 return model; 516 } 517 518 /** 519 * Checks to see if the current row is available. This is useful when the total number of rows is not known. 520 * 521 * @see EditableModel#isRowAvailable 522 * @return true if the current row is available. 523 */ 524 public final boolean isRowAvailable() { 525 return getEditableModel().isRowAvailable(); 526 } 527 528 /** 529 * Checks to see if the current row is modified. 530 * 531 * @see EditableModel#isRowModified 532 * @return true if the current row is modified. 533 */ 534 public final boolean isRowModified() { 535 return getEditableModel().isRowModified(); 536 } 537 538 /** 539 * Gets the total number of rows in this table. 540 * 541 * @see EditableModel#getRowCount 542 * @return -1 if the total number is not known. 543 */ 544 public final int getRowCount() { 545 return getEditableModel().getRowCount(); 546 } 547 548 /** 549 * Gets the index of the current row. 550 * 551 * @see EditableModel#getRowIndex 552 * @return -1 if the current row is unavailable. 553 */ 554 public final int getRowIndex() { 555 return getEditableModel().getRowIndex(); 556 } 557 558 /** 559 * Gets the rowKey of the current row. 560 * 561 * @see EditableModel#getRowKey 562 * @return null if the current row is unavailable. 563 */ 564 public final Integer getRowKey() { 565 return getEditableModel().getRowKey(); 566 } 567 568 /** 569 * Gets the data for the current row. 570 * 571 * @see EditableModel#getRowData 572 * @return null if the current row is unavailable 573 */ 574 public final Object getRowData() { 575 EditableModel model = getEditableModel(); 576 // we need to call isRowAvailable() here because the 1.0 sun RI was 577 // throwing exceptions when getRowData() was called with rowIndex=-1 578 return model.isRowAvailable() ? model.getRowData() : null; 579 } 580 581 /** 582 * Makes a row current. 583 * <p> 584 * This method calls {@link #preRowDataChange} and {@link #postRowDataChange} as appropriate. 585 * 586 * @see EditableModel#setRowIndex 587 * @param rowIndex The rowIndex of the row that should be made current. Use -1 to clear the current row. 588 */ 589 public void setRowIndex(int rowIndex) { 590 // do not change row index if not needed 591 int current = getEditableModel().getRowIndex(); 592 if (current != rowIndex) { 593 Object oldValue = saveRequestMapModelValue(); 594 preRowDataChange(); 595 getEditableModel().setRowIndex(rowIndex); 596 postRowDataChange(oldValue); 597 } 598 } 599 600 /** 601 * Makes a row current. 602 * <p> 603 * This method calls {@link #preRowDataChange} and {@link #postRowDataChange} as appropriate. 604 * 605 * @see EditableModel#setRowKey 606 * @param rowKey The rowKey of the row that should be made current. Use null to clear the current row. 607 */ 608 public void setRowKey(Integer rowKey) { 609 // do not change row key if not needed 610 Integer current = getEditableModel().getRowKey(); 611 if ((current != null && !current.equals(rowKey)) || (current == null && rowKey != null)) { 612 // XXX AT: do not save state before setting row key as current index 613 // may not point to the same object anymore (XXX: need to handle this 614 // better, as events may change the data too, in which case we would 615 // want the state to be saved). 616 // preRowDataChange(); 617 Object oldRequestValue = saveRequestMapModelValue(); 618 getEditableModel().setRowKey(rowKey); 619 postRowDataChange(oldRequestValue); 620 } 621 } 622 623 /** 624 * Records a value modification. 625 * 626 * @see EditableModel#recordValueModified 627 */ 628 public final void recordValueModified(int index, Object newValue) { 629 getEditableModel().recordValueModified(index, newValue); 630 } 631 632 /** 633 * Adds a value to the end of the editable model. 634 * 635 * @param value the value to add 636 * @return true if value was added. 637 */ 638 public boolean addValue(Object value) { 639 return getEditableModel().addValue(value); 640 } 641 642 /** 643 * Inserts value at given index on the editable model. 644 * 645 * @throws IllegalArgumentException if model does not handle this index. 646 */ 647 public void insertValue(int index, Object value) { 648 getEditableModel().insertValue(index, value); 649 } 650 651 /** 652 * Modifies value at given index on the editable model. 653 * 654 * @return the old value at that index. 655 * @throws IllegalArgumentException if model does not handle one of given indexes. 656 */ 657 public Object moveValue(int fromIndex, int toIndex) { 658 throw new NotImplementedException(); 659 } 660 661 /** 662 * Removes value at given index on the editable model. 663 * 664 * @return the old value at that index. 665 * @throws IllegalArgumentException if model does not handle this index. 666 */ 667 public Object removeValue(int index) { 668 return getEditableModel().removeValue(index); 669 } 670 671 /** 672 * Gets model name exposed in request map. 673 */ 674 public String getModel() { 675 if (model != null) { 676 return model; 677 } 678 ValueExpression ve = getValueExpression("model"); 679 if (ve != null) { 680 try { 681 return (String) ve.getValue(getFacesContext().getELContext()); 682 } catch (ELException e) { 683 throw new FacesException(e); 684 } 685 } else { 686 return null; 687 } 688 } 689 690 /** 691 * Sets model name exposed in request map. 692 */ 693 public void setModel(String model) { 694 this.model = model; 695 } 696 697 /** 698 * Gets template to be used when adding new values to the model. 699 */ 700 public Object getTemplate() { 701 if (template != null) { 702 return template; 703 } 704 ValueExpression ve = getValueExpression("template"); 705 if (ve != null) { 706 try { 707 Object res = ve.getValue(getFacesContext().getELContext()); 708 if (res instanceof String) { 709 // try to resolve a second time in case it's an expression 710 res = ComponentTagUtils.resolveElExpression(getFacesContext(), (String) res); 711 } 712 return res; 713 } catch (ELException e) { 714 throw new FacesException(e); 715 } 716 } else { 717 return null; 718 } 719 } 720 721 /** 722 * Sets template to be used when adding new values to the model. 723 */ 724 public final void setTemplate(Object template) { 725 this.template = template; 726 } 727 728 /** 729 * Gets boolean stating if diff must be used when saving the value submitted. 730 */ 731 public Boolean getDiff() { 732 if (diff != null) { 733 return diff; 734 } 735 ValueExpression ve = getValueExpression("diff"); 736 if (ve != null) { 737 try { 738 return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); 739 } catch (ELException e) { 740 throw new FacesException(e); 741 } 742 } else { 743 // default value 744 return false; 745 } 746 } 747 748 /** 749 * Sets boolean stating if diff must be used when saving the value submitted. 750 */ 751 public void setDiff(Boolean diff) { 752 this.diff = diff; 753 } 754 755 public Integer getNumber() { 756 if (number != null) { 757 return number; 758 } 759 ValueExpression ve = getValueExpression("number"); 760 if (ve != null) { 761 try { 762 return ((Number) ve.getValue(getFacesContext().getELContext())).intValue(); 763 } catch (ELException e) { 764 throw new FacesException(e); 765 } 766 } else { 767 // default value 768 return null; 769 } 770 } 771 772 public void setNumber(Integer number) { 773 this.number = number; 774 } 775 776 public Boolean getRemoveEmpty() { 777 if (removeEmpty != null) { 778 return removeEmpty; 779 } 780 ValueExpression ve = getValueExpression("removeEmpty"); 781 if (ve != null) { 782 try { 783 return !Boolean.FALSE.equals(ve.getValue(getFacesContext().getELContext())); 784 } catch (ELException e) { 785 throw new FacesException(e); 786 } 787 } else { 788 // default value 789 return false; 790 } 791 } 792 793 public void setRemoveEmpty(Boolean removeEmpty) { 794 this.removeEmpty = removeEmpty; 795 } 796 797 /** 798 * Override container client id resolution to handle recursion. 799 */ 800 @Override 801 @SuppressWarnings("deprecation") 802 public String getContainerClientId(FacesContext context) { 803 String id = super.getClientId(context); 804 // avoid trigger of editable model creation for id retrieval 805 InternalState iState = getInternalState(false); 806 if (iState == null || iState._model == null) { 807 return id; 808 } 809 int index = getRowIndex(); 810 if (index != -1) { 811 id += NamingContainer.SEPARATOR_CHAR + String.valueOf(index); 812 } 813 return id; 814 } 815 816 @Override 817 public String getRendererType() { 818 return null; 819 } 820 821 @Override 822 public void setRendererType(String rendererType) { 823 // do nothing 824 } 825 826 @Override 827 public final void encodeBegin(FacesContext context) throws IOException { 828 initializeState(false); 829 flushCachedModel(); 830 831 super.encodeBegin(context); 832 } 833 834 @Override 835 public void encodeEnd(FacesContext context) throws IOException { 836 super.encodeEnd(context); 837 } 838 839 @Override 840 public boolean getRendersChildren() { 841 return true; 842 } 843 844 /** 845 * Repeatedly render the children as many times as needed. 846 */ 847 @Override 848 public void encodeChildren(final FacesContext context) throws IOException { 849 if (!isRendered()) { 850 return; 851 } 852 853 processFacetsAndChildren(context, PhaseId.RENDER_RESPONSE); 854 encodeTemplate(context); 855 } 856 857 protected void encodeTemplate(FacesContext context) throws IOException { 858 // helper method to be overriden by UIJavascriptList 859 } 860 861 // events 862 /** 863 * Delivers a wrapped event to the appropriate component. If the event is a special wrapped event, it is unwrapped. 864 * 865 * @param event a FacesEvent 866 * @throws javax.faces.event.AbortProcessingException 867 */ 868 @Override 869 public void broadcast(FacesEvent event) { 870 if (event instanceof EditableModelRowEvent) { 871 EditableModelRowEvent rowEvent = (EditableModelRowEvent) event; 872 Integer old = getRowKey(); 873 Object requestMapValue = saveRequestMapModelValue(); 874 try { 875 FacesContext context = FacesContext.getCurrentInstance(); 876 setRowKey(rowEvent.getKey()); 877 UIComponent source = rowEvent.getComponent(); 878 FacesEvent wrapped = rowEvent.getEvent(); 879 UIComponent compositeParent = null; 880 try { 881 if (!UIComponent.isCompositeComponent(source)) { 882 compositeParent = UIComponent.getCompositeComponentParent(source); 883 } 884 if (compositeParent != null) { 885 compositeParent.pushComponentToEL(context, null); 886 } 887 source.pushComponentToEL(context, null); 888 wrapped.getComponent().broadcast(wrapped); 889 } finally { 890 source.popComponentFromEL(context); 891 if (compositeParent != null) { 892 compositeParent.popComponentFromEL(context); 893 } 894 } 895 setRowKey(old); 896 } finally { 897 restoreRequestMapModelValue(requestMapValue); 898 } 899 } else { 900 super.broadcast(event); 901 } 902 } 903 904 /** 905 * Queues an event. If there is a currency set on this table, then the event will be wrapped so that when it is 906 * finally delivered, the correct currency will be restored. 907 * 908 * @param event a FacesEvent 909 */ 910 @Override 911 public void queueEvent(FacesEvent event) { 912 // we want to wrap up the event so we can execute it in the correct 913 // context (with the correct rowKey/rowData): 914 Integer currencyKey = getRowKey(); 915 if (currencyKey == null) { 916 // not a row event 917 super.queueEvent(event); 918 } else { 919 super.queueEvent(new EditableModelRowEvent(this, event, currencyKey)); 920 } 921 } 922 923 private boolean requiresRowIteration(VisitContext ctx) { 924 return !ctx.getHints().contains(VisitHint.SKIP_ITERATION); 925 } 926 927 private boolean visitRows(VisitContext context, VisitCallback callback, boolean visitRows) { 928 // Iterate over our children, once per row 929 int processed = 0; 930 int oldIndex = getRowIndex(); 931 int rowIndex = getRowIndex(); 932 boolean rowChanged = false; 933 int rows = 0; 934 if (visitRows) { 935 rowIndex = -1; 936 rows = getRowCount(); 937 } 938 939 Object requestMapValue = saveRequestMapModelValue(); 940 try { 941 942 while (true) { 943 944 if (visitRows) { 945 if ((rows > 0) && (++processed > rows)) { 946 break; 947 } 948 // Expose the current row in the specified 949 // request attribute 950 setRowIndex(++rowIndex); 951 rowChanged = true; 952 if (!isRowAvailable()) { 953 break; // Scrolled past the last row 954 } 955 } 956 957 // Visit as required on the *children* 958 if (getChildCount() > 0) { 959 for (UIComponent kid : getChildren()) { 960 if (kid.getChildCount() > 0) { 961 for (UIComponent grandkid : kid.getChildren()) { 962 if (grandkid.visitTree(context, callback)) { 963 return true; 964 } 965 } 966 } 967 } 968 } 969 970 if (!visitRows) { 971 break; 972 } 973 974 } 975 } finally { 976 if (rowChanged) { 977 setRowIndex(oldIndex); 978 restoreRequestMapModelValue(requestMapValue); 979 } 980 } 981 982 return false; 983 } 984 985 private boolean doVisitChildren(VisitContext context, boolean visitRows) { 986 987 // Just need to check whether there are any ids under this 988 // subtree. Make sure row index is cleared out since 989 // getSubtreeIdsToVisit() needs our row-less client id. 990 if (visitRows) { 991 setRowIndex(-1); 992 } 993 Collection<String> idsToVisit = context.getSubtreeIdsToVisit(this); 994 assert idsToVisit != null; 995 996 // All ids or non-empty collection means we need to visit our children. 997 return (!idsToVisit.isEmpty()); 998 } 999 1000 /** 1001 * Rough adapt of the UI data behaviour. 1002 */ 1003 @Override 1004 public boolean visitTree(VisitContext context, VisitCallback callback) { 1005 if (!isVisitable(context)) { 1006 return false; 1007 } 1008 1009 FacesContext facesContext = context.getFacesContext(); 1010 boolean visitRows = requiresRowIteration(context); 1011 1012 int oldRowIndex = -1; 1013 if (visitRows) { 1014 oldRowIndex = getRowIndex(); 1015 if (oldRowIndex != -1) { 1016 setRowIndex(-1); 1017 } 1018 } 1019 1020 pushComponentToEL(facesContext, null); 1021 1022 try { 1023 1024 VisitResult result = context.invokeVisitCallback(this, callback); 1025 if (result == VisitResult.COMPLETE) { 1026 return true; 1027 } 1028 if ((result == VisitResult.ACCEPT) && doVisitChildren(context, visitRows)) { 1029 if (visitRows(context, callback, visitRows)) { 1030 return true; 1031 } 1032 } 1033 } finally { 1034 popComponentFromEL(facesContext); 1035 if (visitRows && oldRowIndex != -1) { 1036 setRowIndex(oldRowIndex); 1037 } 1038 } 1039 1040 // return false to allow the visit to continue 1041 return false; 1042 } 1043 1044 @Override 1045 public void processDecodes(FacesContext context) { 1046 if (!isRendered()) { 1047 return; 1048 } 1049 1050 initializeState(false); 1051 1052 flushCachedModel(); 1053 1054 // process this component decode before so that it can help detecting new/deleted rows before applying children 1055 // decodes 1056 decode(context); 1057 1058 pushComponentToEL(context, this); 1059 processFacetsAndChildren(context, PhaseId.APPLY_REQUEST_VALUES); 1060 popComponentFromEL(context); 1061 1062 if (isImmediate()) { 1063 executeValidate(context); 1064 } 1065 } 1066 1067 @Override 1068 public void processValidators(FacesContext context) { 1069 if (!isRendered()) { 1070 return; 1071 } 1072 1073 initializeState(true); 1074 1075 if (!isImmediate()) { 1076 executeValidate(context); 1077 } 1078 } 1079 1080 @Override 1081 public void processUpdates(FacesContext context) { 1082 if (!isRendered()) { 1083 return; 1084 } 1085 1086 initializeState(true); 1087 1088 try { 1089 updateModel(context); 1090 } catch (RuntimeException e) { 1091 context.renderResponse(); 1092 throw e; 1093 } 1094 1095 if (!isValid()) { 1096 context.renderResponse(); 1097 } else { 1098 // force reset 1099 resetCachedModel(); 1100 } 1101 } 1102 1103 /** 1104 * Overridden to handle diff boolean value, see NXP-16515. 1105 */ 1106 public void updateModel(FacesContext context) { 1107 1108 if (context == null) { 1109 throw new NullPointerException(); 1110 } 1111 1112 if (!isValid() || !isLocalValueSet()) { 1113 return; 1114 } 1115 ValueExpression ve = getValueExpression("value"); 1116 if (ve != null) { 1117 Throwable caught = null; 1118 FacesMessage message = null; 1119 try { 1120 Boolean setDiff = getDiff(); 1121 if (setDiff) { 1122 // set list diff instead of the whole list 1123 // FIXME NXP-16515 1124 // EditableModel model = getEditableModel(); 1125 // ve.setValue(context.getELContext(), model.getListDiff()); 1126 ve.setValue(context.getELContext(), getLocalValue()); 1127 } else { 1128 ve.setValue(context.getELContext(), getLocalValue()); 1129 } 1130 setValue(null); 1131 setLocalValueSet(false); 1132 } catch (ELException e) { 1133 caught = e; 1134 String messageStr = e.getMessage(); 1135 Throwable result = e.getCause(); 1136 while (null != result && result.getClass().isAssignableFrom(ELException.class)) { 1137 messageStr = result.getMessage(); 1138 result = result.getCause(); 1139 } 1140 if (null == messageStr) { 1141 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, 1142 MessageFactory.getLabel(context, this)); 1143 } else { 1144 message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr); 1145 } 1146 setValid(false); 1147 } catch (Exception e) { 1148 caught = e; 1149 message = MessageFactory.getMessage(context, UPDATE_MESSAGE_ID, MessageFactory.getLabel(context, this)); 1150 setValid(false); 1151 } 1152 if (caught != null) { 1153 assert message != null; 1154 // PENDING(edburns): verify this is in the spec. 1155 UpdateModelException toQueue = new UpdateModelException(message, caught); 1156 ExceptionQueuedEventContext eventContext = new ExceptionQueuedEventContext(context, toQueue, this, 1157 PhaseId.UPDATE_MODEL_VALUES); 1158 context.getApplication().publishEvent(context, ExceptionQueuedEvent.class, eventContext); 1159 1160 } 1161 1162 } 1163 } 1164 1165 protected void processFacetsAndChildren(final FacesContext context, final PhaseId phaseId) { 1166 List<UIComponent> stamps = getChildren(); 1167 int oldIndex = getRowIndex(); 1168 boolean rowChanged = false; 1169 int end = getRowCount(); 1170 Object requestMapValue = saveRequestMapModelValue(); 1171 try { 1172 int first = 0; 1173 for (int i = first; i < end; i++) { 1174 rowChanged = true; 1175 setRowIndex(i); 1176 if (isRowAvailable()) { 1177 for (UIComponent stamp : stamps) { 1178 processComponent(context, stamp, phaseId); 1179 } 1180 if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 1181 // detect changes during process update phase and fill 1182 // the EditableModel list diff. 1183 // XXX maybe handle new values with js list too 1184 if (isRowModified()) { 1185 recordValueModified(i, getRowData()); 1186 } 1187 } 1188 } else { 1189 break; 1190 } 1191 } 1192 } finally { 1193 if (rowChanged) { 1194 setRowIndex(oldIndex); 1195 restoreRequestMapModelValue(requestMapValue); 1196 } 1197 } 1198 } 1199 1200 protected final void processComponent(FacesContext context, UIComponent component, PhaseId phaseId) { 1201 if (component != null) { 1202 if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { 1203 component.processDecodes(context); 1204 } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) { 1205 component.processValidators(context); 1206 } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 1207 component.processUpdates(context); 1208 } else if (phaseId == PhaseId.RENDER_RESPONSE) { 1209 try { 1210 ComponentSupport.encodeRecursive(context, component); 1211 } catch (IOException err) { 1212 log.error("Error while rendering component " + component); 1213 } 1214 } else { 1215 throw new IllegalArgumentException("Bad PhaseId:" + phaseId); 1216 } 1217 } 1218 } 1219 1220 @SuppressWarnings("rawtypes") 1221 private void executeValidate(FacesContext context) { 1222 try { 1223 pushComponentToEL(context, this); 1224 processFacetsAndChildren(context, PhaseId.PROCESS_VALIDATIONS); 1225 // process updates right away so that list can perform its global validation 1226 processFacetsAndChildren(context, PhaseId.UPDATE_MODEL_VALUES); 1227 popComponentFromEL(context); 1228 1229 EditableModel model = getEditableModel(); 1230 if (model.isDirty()) { 1231 // remove empty values if needed 1232 Boolean removeEmpty = getRemoveEmpty(); 1233 Object data = model.getWrappedData(); 1234 if (removeEmpty && data instanceof List) { 1235 Object template = getTemplate(); 1236 List dataList = (List) data; 1237 for (int i = dataList.size() - 1; i > -1; i--) { 1238 Object item = dataList.get(i); 1239 if (item == null || item.equals(template)) { 1240 model.removeValue(i); 1241 } 1242 } 1243 } 1244 } 1245 1246 Object submitted = model.getWrappedData(); 1247 if (submitted == null) { 1248 // set submitted to empty list to force validation 1249 submitted = Collections.emptyList(); 1250 } 1251 setSubmittedValue(submitted); 1252 validate(context); 1253 1254 } catch (RuntimeException e) { 1255 context.renderResponse(); 1256 throw e; 1257 } 1258 1259 if (!isValid()) { 1260 context.renderResponse(); 1261 } 1262 } 1263 1264 @Override 1265 public boolean invokeOnComponent(FacesContext context, String clientId, ContextCallback callback) 1266 throws FacesException { 1267 if (null == context || null == clientId || null == callback) { 1268 throw new NullPointerException(); 1269 } 1270 1271 // try invoking on list 1272 String myId = super.getClientId(context); 1273 if (clientId.equals(myId)) { 1274 try { 1275 pushComponentToEL(context, UIComponent.getCompositeComponentParent(this)); 1276 callback.invokeContextCallback(context, this); 1277 return true; 1278 } catch (RuntimeException e) { 1279 // TODO what is caught here? 1280 throw new FacesException(e); 1281 } finally { 1282 popComponentFromEL(context); 1283 } 1284 } 1285 1286 List<UIComponent> stamps = getChildren(); 1287 int oldIndex = getRowIndex(); 1288 int end = getRowCount(); 1289 boolean rowChanged = false; 1290 boolean found = false; 1291 Object requestMapValue = saveRequestMapModelValue(); 1292 try { 1293 int first = 0; 1294 for (int i = first; i < end; i++) { 1295 rowChanged = true; 1296 setRowIndex(i); 1297 if (isRowAvailable()) { 1298 for (UIComponent stamp : stamps) { 1299 found = stamp.invokeOnComponent(context, clientId, callback); 1300 } 1301 } else { 1302 break; 1303 } 1304 } 1305 } finally { 1306 if (rowChanged) { 1307 setRowIndex(oldIndex); 1308 restoreRequestMapModelValue(requestMapValue); 1309 } 1310 } 1311 1312 return found; 1313 } 1314 1315}