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