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}