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