| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.myfaces.trinidad.component; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.Serializable; |
| |
| import java.util.AbstractMap; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.faces.FacesException; |
| import javax.faces.application.StateManager; |
| import javax.faces.component.ContextCallback; |
| import javax.faces.component.NamingContainer; |
| import javax.faces.component.UIComponent; |
| import javax.faces.component.visit.VisitCallback; |
| import javax.faces.component.visit.VisitContext; |
| import javax.faces.component.visit.VisitContextWrapper; |
| import javax.faces.component.visit.VisitResult; |
| import javax.faces.context.FacesContext; |
| import javax.faces.event.AbortProcessingException; |
| import javax.faces.event.ComponentSystemEvent; |
| import javax.faces.event.FacesEvent; |
| import javax.faces.event.PhaseId; |
| import javax.faces.render.Renderer; |
| |
| import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFComponent; |
| import org.apache.myfaces.trinidad.bean.FacesBean; |
| import org.apache.myfaces.trinidad.bean.PropertyKey; |
| import org.apache.myfaces.trinidad.context.ComponentContextChange; |
| import org.apache.myfaces.trinidad.context.ComponentContextManager; |
| import org.apache.myfaces.trinidad.context.RequestContext; |
| import org.apache.myfaces.trinidad.event.SelectionEvent; |
| import org.apache.myfaces.trinidad.logging.TrinidadLogger; |
| import org.apache.myfaces.trinidad.model.CollectionModel; |
| import org.apache.myfaces.trinidad.model.LocalRowKeyIndex; |
| import org.apache.myfaces.trinidad.model.RowKeyChangeEvent; |
| import org.apache.myfaces.trinidad.model.RowKeyChangeListener; |
| import org.apache.myfaces.trinidad.model.SortCriterion; |
| import org.apache.myfaces.trinidad.render.ClientRowKeyManager; |
| import org.apache.myfaces.trinidad.render.ClientRowKeyManagerFactory; |
| import org.apache.myfaces.trinidad.util.Args; |
| import org.apache.myfaces.trinidad.util.ComponentUtils; |
| |
| |
| /** |
| * Base class for components that do stamping. |
| * This class set the EL 'var' variable correctly when the rowData changes. |
| * And it wraps events that are queued, so that the correct rowData can be |
| * restored on this component when the event is broadcast. |
| */ |
| @JSFComponent |
| public abstract class UIXCollection extends UIXComponentBase |
| implements NamingContainer |
| { |
| static public final FacesBean.Type TYPE = new FacesBean.Type( |
| UIXComponentBase.TYPE); |
| static public final PropertyKey VAR_KEY = |
| TYPE.registerKey("var", String.class, PropertyKey.CAP_NOT_BOUND); |
| |
| protected UIXCollection(String rendererType) |
| { |
| super(rendererType); |
| } |
| |
| protected UIXCollection() |
| { |
| this(null); |
| } |
| |
| /** |
| * Gets the name of the EL variable used to reference each element of |
| * this collection. Once this component has completed rendering, this |
| * variable is removed (or reverted back to its previous value). |
| */ |
| final public String getVar() |
| { |
| return ComponentUtils.resolveString(getProperty(VAR_KEY)); |
| } |
| |
| /** |
| * Sets the name of the EL variable used to reference each element of |
| * this collection. Once this component has completed rendering, this |
| * variable is removed (or reverted back to its previous value). |
| */ |
| final public void setVar(String var) |
| { |
| setProperty(VAR_KEY, (var)); |
| InternalState iState = _getInternalState(false); |
| if (iState != null) |
| { |
| iState._var = var; |
| } |
| } |
| |
| /** |
| * Queues an event. If there is a currency set on this table, then |
| * the event will be wrapped so that when it is finally delivered, the correct |
| * currency will be restored. |
| * @param event a FacesEvent |
| */ |
| @Override |
| public void queueEvent(FacesEvent event) |
| { |
| if (event.getSource() == this) |
| { |
| // Remember non-SelectionEvents on ourselves. This |
| // is a hack to support validation in tableSelectXyz. |
| if (!(event instanceof SelectionEvent)) |
| { |
| InternalState iState = _getInternalState(true); |
| iState._hasEvent = true; |
| } |
| } |
| |
| // we want to wrap up |
| // the event so we can execute it in the correct context (with the correct |
| // rowKey/rowData): |
| Object currencyKey = getRowKey(); |
| event = new TableRowEvent(this, event, currencyKey); |
| |
| // Queue a CollectionContextEvent in order to allow this class to setup the component change |
| // before sub-classes attempt to process the table row event instance. |
| super.queueEvent(new CollectionContextEvent(this, event)); |
| } |
| |
| /** |
| * Delivers a wrapped event to the appropriate component. |
| * If the event is a special wrapped event, it is unwrapped. |
| * @param event a FacesEvent |
| * @throws javax.faces.event.AbortProcessingException |
| */ |
| @Override |
| public void broadcast(FacesEvent event) |
| throws AbortProcessingException |
| { |
| FacesContext context = getFacesContext(); |
| |
| // Unwrap CollectionContextEvent events so that the original event is broadcast |
| // within a component change event context. |
| if (event instanceof CollectionContextEvent) |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| CollectionContextEvent wrapperEvent = (CollectionContextEvent) event; |
| wrapperEvent.broadcastWrappedEvent(context); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| else |
| { |
| // For "TableRowEvents", set up the data before firing the |
| // event to the actual component. |
| if (event instanceof TableRowEvent) |
| { |
| TableRowEvent rowEvent = (TableRowEvent) event; |
| Object old = getRowKey(); |
| setRowKey(rowEvent.getCurrencyKey()); |
| rowEvent.broadcastWrappedEvent(context); |
| setRowKey(old); |
| } |
| else |
| { |
| super.broadcast(event); |
| } |
| } |
| } |
| |
| /** |
| * Decodes this component before decoding the facets. |
| * Decodes the children as many times as they are stamped. |
| * @param context the FacesContext |
| */ |
| @Override |
| public final void processDecodes(FacesContext context) |
| { |
| if (context == null) |
| throw new NullPointerException(); |
| |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| _init(); |
| |
| InternalState iState = _getInternalState(true); |
| iState._isFirstRender = false; |
| |
| if (!isRendered()) |
| return; |
| |
| __flushCachedModel(); |
| |
| // Make sure _hasEvent is false. |
| iState._hasEvent = false; |
| |
| // =-=AEW Because I'm getting the state in decode(), I need |
| // to do it before iterating over the children - otherwise, |
| // they'll be working off the wrong startIndex. When state |
| // management is integrated, I can likely put this back in the |
| // usual order |
| |
| // Process this component itself |
| decode(context); |
| |
| // Process all facets and children of this component |
| decodeChildren(context); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| @Override |
| protected void decodeChildrenImpl(FacesContext context) |
| { |
| processFacetsAndChildren(context, PhaseId.APPLY_REQUEST_VALUES); |
| } |
| |
| @Override |
| protected void validateChildrenImpl(FacesContext context) |
| { |
| processFacetsAndChildren(context, PhaseId.PROCESS_VALIDATIONS); |
| } |
| |
| @Override |
| protected void updateChildrenImpl(FacesContext context) |
| { |
| processFacetsAndChildren(context, PhaseId.UPDATE_MODEL_VALUES); |
| } |
| |
| /** |
| * Resets this component's stamps to their |
| * uninitialized state. This is useful when the user wants to |
| * undo any edits made to an editable table. |
| */ |
| public void resetStampState() |
| { |
| InternalState iState = _getInternalState(true); |
| // TODO: this is over kill. for eg, It clears out any toggled showDetails. |
| Object initKey = _getCurrencyKeyForInitialStampState(); |
| // do not clear the initial stamp state: a subtle bug could |
| // result where the initial state of each component is gone, so we |
| // fail to roll back to the initial default values |
| if (iState._stampState != null) |
| iState._stampState.clear(initKey); |
| } |
| |
| @Override |
| public Object processSaveState(FacesContext context) |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| _stateSavingCurrencyKey = _resetCurrencyKeyForStateSaving(context); |
| |
| Object savedState = super.processSaveState(context); |
| |
| _restoreCurrencyKeyForStateSaving(_stateSavingCurrencyKey); |
| _resetInternalState(); |
| |
| return savedState; |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| @Override |
| public Object saveState(FacesContext context) |
| { |
| // _stampState is stored as an instance variable, so it isn't |
| // automatically saved |
| Object superState = super.saveState(context); |
| final Object stampState, clientKeyMgr, idToIndexMap; |
| |
| // becareful not to create the internal state too early: |
| // otherwise, the internal state will be shared between |
| // nested table stamps: |
| InternalState iState = _getInternalState(false); |
| if (iState != null) |
| { |
| stampState = iState._stampState; |
| clientKeyMgr = iState._clientKeyMgr; |
| idToIndexMap = iState._idToIndexMap; |
| } |
| else |
| { |
| stampState = null; |
| clientKeyMgr = null; |
| idToIndexMap = null; |
| } |
| |
| if ((superState != null) || (stampState != null) || |
| (clientKeyMgr != null) || (idToIndexMap != null)) |
| return new Object[]{superState, stampState, clientKeyMgr, idToIndexMap}; |
| return null; |
| } |
| |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public void restoreState(FacesContext context, Object state) |
| { |
| final Object superState, stampState, clientKeyMgr, idToIndexMap; |
| Object[] array = (Object[]) state; |
| if (array != null) |
| { |
| superState = array[0]; |
| stampState = array[1]; |
| clientKeyMgr = array[2]; |
| idToIndexMap = array[3]; |
| } |
| else |
| { |
| superState = null; |
| stampState = null; |
| clientKeyMgr = null; |
| idToIndexMap = null; |
| } |
| super.restoreState(context, superState); |
| |
| if ((stampState != null) || (clientKeyMgr != null) || (idToIndexMap != null)) |
| { |
| InternalState iState = _getInternalState(true); |
| iState._stampState = (StampState) stampState; |
| iState._clientKeyMgr = (ClientRowKeyManager) clientKeyMgr; |
| iState._idToIndexMap = (Map<String, String>) idToIndexMap; |
| } |
| else |
| { |
| // becareful not to force creation of the internal state |
| // too early: |
| InternalState iState = _getInternalState(false); |
| if (iState != null) |
| { |
| iState._stampState = null; |
| iState._clientKeyMgr = null; |
| iState._idToIndexMap = null; |
| } |
| } |
| } |
| |
| /** |
| * Checks to see if the current row is available. This is useful when the |
| * total number of rows is not known. |
| * @see CollectionModel#isRowAvailable() |
| * @return true iff the current row is available. |
| */ |
| public final boolean isRowAvailable() |
| { |
| return getCollectionModel().isRowAvailable(); |
| } |
| |
| /** |
| * Check for an available row by row key. |
| * @param rowKey the row key for the row to check. |
| * @return true if a value exists; false otherwise. |
| */ |
| public final boolean isRowAvailable(Object rowKey) |
| { |
| return getCollectionModel().isRowAvailable(rowKey); |
| } |
| |
| /** |
| * Get row data by row key. |
| * @param rowKey the row key for the row to get data. |
| * @return row data |
| */ |
| public final Object getRowData(Object rowKey) |
| { |
| return getCollectionModel().getRowData(rowKey); |
| } |
| |
| /** |
| * Check if a range of rows is available starting from the current position |
| * @param rowCount number of rows to check |
| * @return true if all rows in range are available |
| */ |
| public final boolean areRowsAvailable(int rowCount) |
| { |
| return getCollectionModel().areRowsAvailable(rowCount); |
| } |
| |
| /** |
| * Check if a range of rows is available from a starting index without |
| * requiring the client to iterate over the rows |
| * @param startIndex the starting index for the range |
| * @param rowCount number of rows to check |
| * @return true if all rows in range are available |
| */ |
| public final boolean areRowsAvailable(int startIndex, int rowCount) |
| { |
| return getCollectionModel().areRowsAvailable(startIndex, rowCount); |
| } |
| |
| /** |
| * Check if a range of rows is available from a starting row key without |
| * requiring the client to iterate over the rows |
| * @param startRowKey the starting row key for the range |
| * @param rowCount number of rows to check |
| * @return true if all rows in range are available |
| */ |
| public final boolean areRowsAvailable(Object startRowKey, int rowCount) |
| { |
| return getCollectionModel().areRowsAvailable(startRowKey, rowCount); |
| } |
| |
| |
| |
| /** |
| * Gets the total number of rows in this table. |
| * @see CollectionModel#getRowCount |
| * @return -1 if the total number is not known. |
| */ |
| public final int getRowCount() |
| { |
| return getCollectionModel().getRowCount(); |
| } |
| |
| /** |
| * Gets the index of the current row. |
| * @see CollectionModel#getRowIndex |
| * @return -1 if the current row is unavailable. |
| */ |
| public final int getRowIndex() |
| { |
| return getCollectionModel().getRowIndex(); |
| } |
| |
| /** |
| * Gets the rowKey of the current row. |
| * @see CollectionModel#getRowKey |
| * @return null if the current row is unavailable. |
| */ |
| public final Object getRowKey() |
| { |
| InternalState iState = _getInternalState(true); |
| if (iState._currentRowKey == _NULL) |
| { |
| // See bug 4534104. |
| // Sometimes the rowKey for a particular row changes during update model |
| // (this happens in ADFM if you edit the primary key of a row). |
| // It is bad if the rowKey changes after _restoreStampState() and |
| // before _saveStampState(). Therefore, we cache it: |
| iState._currentRowKey = getCollectionModel().getRowKey(); |
| iState._model.addRowKeyChangeListener(iState); |
| } |
| |
| return iState._currentRowKey; |
| } |
| |
| /** |
| * Gets the data for the current row. |
| * @see CollectionModel#getRowData(int) |
| * @return null if the current row is unavailable |
| */ |
| public final Object getRowData() |
| { |
| CollectionModel model = getCollectionModel(); |
| // we need to call isRowAvailable() here because the 1.0 sun RI was |
| // throwing exceptions when getRowData() was called with rowIndex=-1 |
| return model.isRowAvailable() ? model.getRowData() : null; |
| } |
| |
| /** |
| * Checks to see if the row at the given index is available. |
| * @see CollectionModel#isRowAvailable(int) |
| * @param rowIndex the index of the row to check. |
| * @return true if data for the row exists. |
| */ |
| public boolean isRowAvailable(int rowIndex) |
| { |
| return getCollectionModel().isRowAvailable(rowIndex); |
| } |
| |
| /** |
| * Gets the rowData at the given index. |
| * @see CollectionModel#getRowData(int) |
| * @param rowIndex the index of the row to get data from. |
| * @return the data for the given row. |
| */ |
| public Object getRowData(int rowIndex) |
| { |
| return getCollectionModel().getRowData(rowIndex); |
| } |
| |
| /** |
| * Gets the EL variable name to use to expose the varStatusMap. |
| * @see #createVarStatusMap() |
| */ |
| public abstract String getVarStatus(); |
| |
| /** |
| * Makes a row current. |
| * This method calls {@link #preRowDataChange} and |
| * {@link #postRowDataChange} as appropriate. |
| * @see CollectionModel#setRowKey |
| * @param rowKey The rowKey of the row that should be made current. Use null |
| * to clear the current row. |
| */ |
| public void setRowKey(Object rowKey) |
| { |
| _verifyComponentInContext(); |
| |
| preRowDataChange(); |
| getCollectionModel().setRowKey(rowKey); |
| postRowDataChange(); |
| if (_LOG.isFine() && (rowKey != null) && (!isRowAvailable())) |
| _LOG.fine("no row available for rowKey:"+rowKey); |
| } |
| |
| /** |
| * Makes a row current. |
| * This method calls {@link #preRowDataChange} and |
| * {@link #postRowDataChange} as appropriate. |
| * @see CollectionModel#setRowIndex |
| * @param rowIndex The rowIndex of the row that should be made current. Use -1 |
| * to clear the current row. |
| */ |
| public void setRowIndex(int rowIndex) |
| { |
| _verifyComponentInContext(); |
| |
| preRowDataChange(); |
| getCollectionModel().setRowIndex(rowIndex); |
| postRowDataChange(); |
| if (_LOG.isFine() && (rowIndex != -1) && (!isRowAvailable())) |
| _LOG.fine("no row available for rowIndex:"+rowIndex); |
| } |
| |
| /** |
| * @param property a property name in the model |
| * @return true if the model is sortable by the given property. |
| * @see CollectionModel#isSortable |
| */ |
| public final boolean isSortable(String property) |
| { |
| return getCollectionModel().isSortable(property); |
| } |
| |
| /** |
| * Sorts this collection by the given criteria. |
| * @param criteria Each element in this List must be of type SortCriterion. |
| * @see org.apache.myfaces.trinidad.model.SortCriterion |
| * @see CollectionModel#setSortCriteria |
| */ |
| public void setSortCriteria(List<SortCriterion> criteria) |
| { |
| getCollectionModel().setSortCriteria(criteria); |
| } |
| |
| /** |
| * Gets the criteria that this collection is sorted by. |
| * @return each element in this List is of type SortCriterion. |
| * An empty list is returned if this collection is not sorted. |
| * @see org.apache.myfaces.trinidad.model.SortCriterion |
| * @see CollectionModel#getSortCriteria |
| */ |
| public final List<SortCriterion> getSortCriteria() |
| { |
| return getCollectionModel().getSortCriteria(); |
| } |
| |
| /** |
| * Clear the rowKey-to-currencyString cache. |
| * The cache is not cleared immediately; instead it will be cleared |
| * when {@link #encodeBegin(FacesContext)} is called. |
| * @deprecated Have your Renderer implement {@link ClientRowKeyManagerFactory} |
| * and create your own {@link ClientRowKeyManager} instances. Then you can |
| * manage the lifecycle of each key inside your ClientRowKeyManager. |
| */ |
| @Deprecated |
| protected void clearCurrencyStringCache() |
| { |
| _getInternalState(true)._clearTokenCache = true; |
| } |
| |
| /** |
| * Clears all the currency strings. |
| */ |
| @Override |
| public final void encodeBegin(FacesContext context) throws IOException |
| { |
| _setupContextChange(); |
| boolean teardown = true; |
| try |
| { |
| _init(); |
| |
| InternalState istate = _getInternalState(true); |
| // we must not clear the currency cache everytime. only clear |
| // it in response to specific events: bug 4773659 |
| |
| // TODO all this code should be removed and moved into the renderer: |
| if (istate._clearTokenCache) |
| { |
| istate._clearTokenCache = false; |
| ClientRowKeyManager keyMgr = getClientRowKeyManager(); |
| if (keyMgr instanceof DefaultClientKeyManager) |
| ((DefaultClientKeyManager) keyMgr).clear(); |
| } |
| __flushCachedModel(); |
| |
| Object assertKey = null; |
| assert ((assertKey = getRowKey()) != null) || true; |
| __encodeBegin(context); |
| // make sure that the rendering code preserves the currency: |
| assert _assertKeyPreserved(assertKey) : "CurrencyKey not preserved"; |
| |
| teardown = false; |
| } |
| finally |
| { |
| if (teardown) |
| { |
| // Tear down on errors & exceptions |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| @Override |
| public void encodeEnd(FacesContext context) throws IOException |
| { |
| try |
| { |
| Object assertKey = null; |
| assert ((assertKey = getRowKey()) != null) || true; |
| super.encodeEnd(context); |
| // make sure that the rendering code preserves the currency: |
| assert _assertKeyPreserved(assertKey) : "CurrencyKey not preserved"; |
| } |
| finally |
| { |
| _tearDownContextChange(); |
| } |
| } |
| |
| @Override |
| protected void setupVisitingContext(FacesContext context) |
| { |
| super.setupVisitingContext(context); |
| _setupContextChange(); |
| |
| if (Boolean.TRUE.equals(context.getAttributes().get(StateManager.IS_SAVING_STATE))) |
| { |
| _stateSavingCurrencyKey = _resetCurrencyKeyForStateSaving(context); |
| } |
| } |
| |
| @Override |
| protected void tearDownVisitingContext(FacesContext context) |
| { |
| if (Boolean.TRUE.equals(context.getAttributes().get(StateManager.IS_SAVING_STATE))) |
| { |
| _restoreCurrencyKeyForStateSaving(_stateSavingCurrencyKey); |
| _resetInternalState(); |
| } |
| |
| _tearDownContextChange(); |
| super.tearDownVisitingContext(context); |
| } |
| |
| private boolean _assertKeyPreserved(Object oldKey) |
| { |
| Object newKey = getRowKey(); |
| return (oldKey != null) ? oldKey.equals(newKey) : (newKey == null); |
| } |
| |
| void __encodeBegin(FacesContext context) throws IOException |
| { |
| super.encodeBegin(context); |
| } |
| |
| /** |
| * Checks to see if processDecodes was called. If this returns true |
| * then this is the initial request, and processDecodes has not been called. |
| */ |
| boolean __isFirstRender() |
| { |
| InternalState iState = _getInternalState(true); |
| return iState._isFirstRender; |
| } |
| |
| /** |
| * @deprecated use getClientRowKey |
| * @see #getClientRowKey |
| */ |
| @Deprecated |
| public String getCurrencyString() |
| { |
| return getClientRowKey(); |
| } |
| |
| /** |
| * @deprecated use setClientRowKey |
| * @see #setClientRowKey |
| */ |
| @Deprecated |
| public void setCurrencyString(String currency) |
| { |
| setClientRowKey(currency); |
| } |
| |
| |
| /** |
| * Gets a String version of the current rowkey. |
| * The contents of the String are controlled by the current |
| * {@link ClientRowKeyManager}. |
| * This String can be passed into the {@link #setClientRowKey} method |
| * to restore the current rowData. |
| * The lifetime of this String is short and it may not be valid during |
| * future requests; however, it is guaranteed to be valid |
| * for the next subsequent request. |
| * @see UIXCollection#setClientRowKey(java.lang.String) |
| * @see UIXCollection#getClientRowKeyManager() |
| * @see ClientRowKeyManager#getClientRowKey |
| */ |
| public String getClientRowKey() |
| { |
| // only call getCurrencyKey if we already have a dataModel. |
| // otherwise behave as though no currency was set. |
| // we need to do this because we don't want dataModel created for components |
| // that are not rendered. The faces RI calls getClientId even on components |
| // that are not rendered and this in turn was calling this method: |
| Object currencyObject = _getCurrencyKey(); |
| if (currencyObject == null) |
| return null; |
| |
| Object initKey = _getCurrencyKeyForInitialStampState(); |
| if (_equals(currencyObject, initKey)) |
| return null; |
| |
| FacesContext fc = FacesContext.getCurrentInstance(); |
| String key = getClientRowKeyManager().getClientRowKey(fc, this, currencyObject); |
| return key; |
| } |
| |
| /** |
| * This is a safe way of getting currency keys and not accidentally forcing |
| * the model to execute. When rendered="false" we should never execute the model. |
| * However, the JSF engine calls certain methods when rendered="false" such as |
| * processSaveState and getClientId. |
| * Those methods, in turn, get the CurrencyKey. |
| */ |
| private Object _getCurrencyKey() |
| { |
| // use false so that we don't create an internal state. |
| // if the internal state is created too soon, then the same internal |
| // state will get shared across all nested table instances. |
| // this was causing bug 4616844: |
| InternalState iState = _getInternalState(false); |
| if (iState == null) |
| return null; |
| |
| return (iState._model != null) |
| ? getRowKey() |
| : _getCurrencyKeyForInitialStampState(); |
| } |
| |
| /** |
| * Restores this component's rowData to be what it was when the given |
| * client rowKey string was created. |
| * @see UIXCollection#getClientRowKey() |
| */ |
| public void setClientRowKey(String clientRowKey) |
| { |
| if (clientRowKey == null) |
| { |
| setRowKey(_getCurrencyKeyForInitialStampState()); |
| return; |
| } |
| |
| FacesContext fc = FacesContext.getCurrentInstance(); |
| Object rowkey = getClientRowKeyManager().getRowKey(fc, this, clientRowKey); |
| |
| if (rowkey == null) |
| { |
| _LOG.severe("CANNOT_FIND_ROWKEY",clientRowKey); |
| } |
| else |
| setRowKey(rowkey); |
| } |
| |
| public void processRestoreState( |
| FacesContext context, |
| Object state) |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| super.processRestoreState(context, state); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| public void processUpdates(FacesContext context) |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| super.processUpdates(context); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| public void processValidators(FacesContext context) |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| super.processValidators(context); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| public void processEvent(ComponentSystemEvent event) |
| throws AbortProcessingException |
| { |
| boolean inContextAtMethodInvocation = _inContext; |
| if (!inContextAtMethodInvocation) |
| { |
| _setupContextChange(); |
| } |
| |
| try |
| { |
| super.processEvent(event); |
| } |
| finally |
| { |
| if (!inContextAtMethodInvocation) |
| { |
| _tearDownContextChange(); |
| } |
| } |
| } |
| |
| /** |
| * Gets the client-id of this component, without any NamingContainers. |
| * This id changes depending on the currency Object. |
| * Because this implementation uses currency strings, the local client ID is |
| * not stable for very long. Its lifetime is the same as that of a |
| * currency string. |
| * @see UIXCollection#getClientRowKey() |
| * @return the local clientId |
| */ |
| @Override |
| public final String getContainerClientId(FacesContext context) |
| { |
| String id = getClientId(context); |
| String key = getClientRowKey(); |
| if (key != null) |
| { |
| StringBuilder bld = __getSharedStringBuilder(); |
| bld.append(id).append(NamingContainer.SEPARATOR_CHAR).append(key); |
| id = bld.toString(); |
| } |
| |
| return id; |
| } |
| |
| /** |
| * Prepares this component for a change in the rowData. |
| * This method should be called right before the rowData changes. |
| * It saves the internal states of all the stamps of this component |
| * so that they can be restored when the rowData is reverted. |
| */ |
| protected final void preRowDataChange() |
| { |
| _saveStampState(); |
| InternalState iState = _getInternalState(true); |
| // mark the cached rowKey as invalid: |
| iState._currentRowKey = _NULL; |
| // we don't have cached rowkey, thus remove rowkeychangelistener |
| if (iState._model != null) |
| iState._model.removeRowKeyChangeListener(iState); |
| } |
| |
| /** |
| * Sets up this component to use the new rowData. |
| * This method should be called right after the rowData changes. |
| * It sets up the var EL variable to be the current rowData. |
| * It also sets up the internal states of all the stamps of this component |
| * to match this new rowData. |
| */ |
| protected final void postRowDataChange() |
| { |
| Object rowData = getRowData(); |
| if (_LOG.isFinest() && (rowData == null)) |
| { |
| _LOG.finest("rowData is null at rowIndex:"+getRowIndex()+ |
| " and currencyKey:"+getRowKey()); |
| } |
| |
| InternalState iState = _getInternalState(true); |
| if (rowData == null) |
| { |
| // if the rowData is null, then we will restore the EL 'var' variable |
| // to be whatever the value was, before this component started rendering: |
| if (iState._prevVarValue != _NULL) |
| { |
| _setELVar(iState._var, iState._prevVarValue); |
| iState._prevVarValue = _NULL; |
| } |
| if (iState._prevVarStatus != _NULL) |
| { |
| _setELVar(iState._varStatus, iState._prevVarStatus); |
| iState._prevVarStatus = _NULL; |
| } |
| } |
| else |
| { |
| if (iState._var != null) |
| { |
| Object oldData = _setELVar(iState._var, rowData); |
| if (iState._prevVarValue == _NULL) |
| iState._prevVarValue = oldData; |
| } |
| |
| // varStatus is not set per row. It is only set once. |
| // if _PrevVarStatus has not been assigned, then we have not set the |
| // varStatus yet: |
| if ((iState._varStatus != null) && (iState._prevVarStatus == _NULL)) |
| { |
| Map<String, Object> varStatusMap = createVarStatusMap(); |
| iState._prevVarStatus = _setELVar(iState._varStatus, varStatusMap); |
| } |
| } |
| |
| _restoreStampState(); |
| |
| // ensure the client IDs are reset on the component, otherwise they will not get the |
| // proper stamped IDs. This mirrors the behavior in UIData and follows the JSF specification |
| // on when client IDs are allowed to be cached and when they must be reset |
| List<UIComponent> stamps = getStamps(); |
| |
| for (UIComponent stamp : stamps) |
| UIXComponent.clearCachedClientIds(stamp); |
| } |
| |
| /** |
| * Gets the UIComponents that are considered stamps. |
| * This implementation simply returns the children of this component. |
| * @return each element must be of type UIComponent. |
| */ |
| @SuppressWarnings("unchecked") |
| protected List<UIComponent> getStamps() |
| { |
| return getChildren(); |
| } |
| |
| /** |
| * Gets the currencyObject to setup the rowData to use to build initial |
| * stamp state. |
| * <p> |
| * This allows the collection model to have an initial row key outside of the UIComponent. |
| * Should the model be at a row that is not the first row, the component will restore the row |
| * back to the initial row key instead of a null row key once stamping is done. |
| * </p> |
| */ |
| private Object _getCurrencyKeyForInitialStampState() |
| { |
| InternalState iState = _getInternalState(false); |
| if (iState == null) |
| return null; |
| |
| Object rowKey = iState._initialStampStateKey; |
| return (rowKey == _NULL) ? null : rowKey; |
| } |
| |
| /** |
| * Saves the state of a stamp. This method is called when the currency of this |
| * component is changed so that the state of this stamp can be preserved, before |
| * the stamp is updated with the state corresponding to the new currency. |
| * This method recurses for the children and facets of the stamp. |
| * @return this object must be Serializable if client-side state saving is |
| * used. |
| */ |
| @SuppressWarnings("unchecked") |
| protected Object saveStampState(FacesContext context, UIComponent stamp) |
| { |
| if (stamp.isTransient()) |
| return null; |
| |
| boolean needsTearDownContext = false; |
| |
| if(stamp instanceof FlattenedComponent && stamp instanceof UIXComponent) |
| { |
| ((UIXComponent)stamp).setupVisitingContext(context); |
| needsTearDownContext = true; |
| } |
| |
| Object[] state = null; |
| |
| try |
| { |
| // The structure we will use is: |
| // 0: state of the stamp |
| // 1: state of the children (a map from child's id to its state) |
| // 2: state of the facets (a map from facet name to its state) |
| // If there is no facet state, we have a two-element array |
| // If there is no facet state or child state, we have a one-elment array |
| // If there is no state at all, we return null |
| |
| Object stampState = StampState.saveStampState(context, stamp); |
| |
| // StampState can never EVER be an Object array, as if we do, |
| // we have no possible way of identifying the difference between |
| // just having stamp state, and having stamp state + child/facet state |
| assert(!(stampState instanceof Object[])); |
| |
| int facetCount = stamp.getFacetCount(); |
| |
| if (facetCount > 0) |
| { |
| Map<String, Object> facetState = null; |
| |
| Map<String, UIComponent> facetMap = stamp.getFacets(); |
| |
| for(Map.Entry<String, UIComponent> entry : facetMap.entrySet()) |
| { |
| Object singleFacetState = saveStampState(context, entry.getValue()); |
| if (singleFacetState == null) |
| continue; |
| |
| // Don't bother allocating anything until we have some non-null |
| // facet state |
| if (facetState == null) |
| { |
| facetState = new HashMap<String, Object>(facetCount); |
| } |
| |
| facetState.put(entry.getKey(), singleFacetState); |
| } |
| |
| // OK, we had something: allocate the state array to three |
| // entries, and insert the facet state at position 2 |
| if (facetState != null) |
| { |
| state = new Object[3]; |
| state[2] = facetState; |
| } |
| } |
| |
| // If we have any children, iterate through the array, |
| // saving state |
| Object childState = StampState.saveChildStampState(context, |
| stamp, |
| this); |
| if (childState != null) |
| { |
| // If the state hasn't been allocated yet, we only |
| // need a two-element array |
| if (state == null) |
| state = new Object[2]; |
| state[1] = childState; |
| } |
| |
| // If we don't have an array, just return the stamp |
| // state |
| if (state == null) |
| return stampState; |
| |
| // Otherwise, store the stamp state at index 0, and return |
| state[0] = stampState; |
| } |
| finally |
| { |
| if(needsTearDownContext) |
| ((UIXComponent)stamp).tearDownVisitingContext(context); |
| } |
| return state; |
| } |
| |
| /** |
| * Restores the state of a stamp. This method is called after the currency of this |
| * component is changed so that the state of this stamp can be changed |
| * to match the new currency. |
| * This method recurses for the children and facets of the stamp. |
| */ |
| @SuppressWarnings("unchecked") |
| protected void restoreStampState(FacesContext context, UIComponent stamp, |
| Object stampState) |
| { |
| // No state for the component - return |
| if (stampState == null) |
| { |
| return; |
| } |
| |
| // If this isn't an Object array, then it's a component with state |
| // of its own, but no child/facet state - so restore and be done |
| if (!(stampState instanceof Object[])) |
| { |
| StampState.restoreStampState(context, stamp, stampState); |
| // NOTE early return |
| return; |
| } |
| |
| Object[] state = (Object[]) stampState; |
| int stateSize = state.length; |
| // We always have at least one element if we get to here |
| assert(stateSize >= 1); |
| |
| StampState.restoreStampState(context, stamp, state[0]); |
| |
| |
| // If there's any facet state, restore it |
| if (stateSize >= 3 && (state[2] instanceof Map)) |
| { |
| Map<String, Object> facetStateMap = (Map<String, Object>) state[2]; |
| // This had better be non-null, otherwise we never |
| // should have allocated a three-element map! |
| assert(facetStateMap != null); |
| |
| for (String facetName : facetStateMap.keySet()) |
| { |
| Object facetState = facetStateMap.get(facetName); |
| if (facetState != null) |
| restoreStampState(context, stamp.getFacet(facetName), facetState); |
| } |
| } |
| |
| // If there's any child state, restore it |
| if (stateSize >= 2) |
| { |
| StampState.restoreChildStampState(context, |
| stamp, |
| this, |
| state[1]); |
| } |
| } |
| |
| /** |
| * Process a component. |
| * This method calls {@link #processDecodes(FacesContext)}, |
| * {@link #processValidators} or |
| * {@link #processUpdates} |
| * depending on the {#link PhaseId}. |
| */ |
| protected final void processComponent( |
| FacesContext context, |
| UIComponent component, |
| PhaseId phaseId) |
| { |
| if (component != null) |
| { |
| if (phaseId == PhaseId.APPLY_REQUEST_VALUES) |
| component.processDecodes(context); |
| else if (phaseId == PhaseId.PROCESS_VALIDATIONS) |
| component.processValidators(context); |
| else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) |
| component.processUpdates(context); |
| else |
| throw new IllegalArgumentException(_LOG.getMessage( |
| "BAD_PHASEID",phaseId)); |
| } |
| } |
| |
| /** |
| * Process this component's facets and children. |
| * This method should call {@link #processComponent} |
| * as many times as necessary for each facet and child. |
| * {@link #processComponent} |
| * may be called repeatedly for the same child if that child is |
| * being stamped. |
| */ |
| protected abstract void processFacetsAndChildren( |
| FacesContext context, |
| PhaseId phaseId); |
| |
| /** |
| * Gets the CollectionModel to use with this component. |
| */ |
| protected final CollectionModel getCollectionModel() |
| { |
| return getCollectionModel(true); |
| } |
| |
| /** |
| * Gets the ClientRowKeyManager that is used to handle the |
| * {@link #getClientRowKey} and |
| * {@link #setClientRowKey} methods. |
| * If the manager does not already exist a new one is created. |
| * In order to create your own manager, your Renderer (for this component) |
| * must implement |
| * {@link ClientRowKeyManagerFactory} |
| */ |
| public final ClientRowKeyManager getClientRowKeyManager() |
| { |
| // this method must be public, because specific renderers |
| // need access to the ClientRowKeyManager so that they might prune it. |
| |
| InternalState iState = _getInternalState(true); |
| if (iState._clientKeyMgr == null) |
| { |
| FacesContext fc = FacesContext.getCurrentInstance(); |
| Renderer r = getRenderer(fc); |
| iState._clientKeyMgr = (r instanceof ClientRowKeyManagerFactory) |
| ? ((ClientRowKeyManagerFactory) r).createClientRowKeyManager(fc, this) |
| : new DefaultClientKeyManager(); |
| } |
| return iState._clientKeyMgr; |
| } |
| |
| public boolean invokeOnComponent(FacesContext context, |
| String clientId, |
| ContextCallback callback) |
| throws FacesException |
| { |
| boolean invokedComponent; |
| setupVisitingContext(context); |
| |
| try |
| { |
| String thisClientId = getClientId(context); |
| if (clientId.equals(thisClientId)) |
| { |
| if (!_getAndMarkFirstInvokeForRequest(context, clientId)) |
| { |
| // Call _init() since __flushCachedModel() assumes that |
| // selectedRowKeys and disclosedRowKeys are initialized to be non-null |
| _init(); |
| |
| __flushCachedModel(); |
| } |
| |
| RequestContext requestContext = RequestContext.getCurrentInstance(); |
| requestContext.pushCurrentComponent(context, this); |
| pushComponentToEL(context, null); |
| |
| try |
| { |
| callback.invokeContextCallback(context, this); |
| } |
| finally |
| { |
| popComponentFromEL(context); |
| requestContext.popCurrentComponent(context, this); |
| } |
| |
| invokedComponent = true; |
| } |
| else |
| { |
| // If we're on a row, set the currency, and invoke |
| // inside |
| int thisClientIdLength = thisClientId.length(); |
| if (clientId.startsWith(thisClientId) && |
| (clientId.charAt(thisClientIdLength) == NamingContainer.SEPARATOR_CHAR)) |
| { |
| if (!_getAndMarkFirstInvokeForRequest(context, thisClientId)) |
| { |
| // Call _init() since __flushCachedModel() assumes that |
| // selectedRowKeys and disclosedRowKeys are initialized to be non-null |
| _init(); |
| |
| __flushCachedModel(); |
| } |
| |
| String postId = clientId.substring(thisClientIdLength + 1); |
| int sepIndex = postId.indexOf(NamingContainer.SEPARATOR_CHAR); |
| // If there's no separator character afterwards, then this |
| // isn't a row key |
| if (sepIndex < 0) |
| return invokeOnChildrenComponents(context, clientId, callback); |
| else |
| { |
| String currencyString = postId.substring(0, sepIndex); |
| Object rowKey = getClientRowKeyManager().getRowKey(context, this, currencyString); |
| |
| // A non-null rowKey here means we are on a row and we should set currency, otherwise |
| // the client id is for a non-stamped child component in the table/column header/footer. |
| if (rowKey != null) |
| { |
| Object oldRowKey = getRowKey(); |
| try |
| { |
| setRowKey(rowKey); |
| invokedComponent = invokeOnChildrenComponents(context, clientId, callback); |
| } |
| finally |
| { |
| // And restore the currency |
| setRowKey(oldRowKey); |
| } |
| } |
| else |
| { |
| invokedComponent = invokeOnChildrenComponents(context, clientId, callback); |
| } |
| } |
| } |
| else |
| { |
| // clientId isn't in this subtree |
| invokedComponent = false; |
| } |
| } |
| } |
| finally |
| { |
| tearDownVisitingContext(context); |
| } |
| |
| return invokedComponent; |
| } |
| |
| /** |
| * <p> |
| * Override default children visiting code to visit the facets and facets of the columns |
| * before delegating to the <code>visitData</code> to visit the individual rows of data. |
| * </p><p> |
| * Subclasses should override this method if they wish to change the way in which the non-stamped |
| * children are visited. If they wish to change the wash the the stamped children are visited, |
| * they should override <code>visitData</code> instead. |
| * </p> |
| * @param visitContext |
| * @param callback |
| * @return <code>true</code> if all of the children to visit have been visited |
| * @see #visitData |
| */ |
| @Override |
| protected boolean visitChildren( |
| VisitContext visitContext, |
| VisitCallback callback) |
| { |
| return defaultVisitChildren(visitContext, callback); |
| } |
| |
| /** |
| * Performs a non-iterating visit of the children. The default implementation visits all |
| * of the children. If the UIXCollection subclass doesn't visit some of its children in |
| * certain cases, it needs to override this method. |
| * @param visitContext |
| * @param callback |
| * @return |
| */ |
| protected boolean visitChildrenWithoutIterating( |
| VisitContext visitContext, |
| VisitCallback callback) |
| { |
| return visitAllChildren(visitContext, callback); |
| } |
| |
| /** |
| * Default implementation of child visiting of UIXCollection subclasses for cases where a |
| * UIXCollection subclass wants to restore the default implementation that one of its |
| * superclasses have overridden. |
| * @param visitContext |
| * @param callback |
| * @return |
| */ |
| protected final boolean defaultVisitChildren( |
| VisitContext visitContext, |
| VisitCallback callback) |
| { |
| if (ComponentUtils.isSkipIterationVisit(visitContext)) |
| { |
| return visitChildrenWithoutIterating(visitContext, callback); |
| } |
| else |
| { |
| boolean doneVisiting; |
| |
| // Clear out the row index if one is set so that |
| // we start from a clean slate. |
| int oldRowIndex = getRowIndex(); |
| setRowIndex(-1); |
| |
| try |
| { |
| // visit the unstamped children |
| doneVisiting = visitUnstampedFacets(visitContext, callback); |
| |
| if (!doneVisiting) |
| { |
| doneVisiting = _visitStampedColumnFacets(visitContext, callback); |
| |
| // visit the stamped children |
| if (!doneVisiting) |
| { |
| doneVisiting = visitData(visitContext, callback); |
| } |
| } |
| } |
| finally |
| { |
| // restore the original rowIndex |
| setRowIndex(oldRowIndex); |
| } |
| |
| return doneVisiting; |
| } |
| } |
| |
| /** |
| * Hook method for subclasses to override to change the behavior |
| * of how unstamped facets of the UIXCollection are visited. The |
| * Default implementation visits all of the facets of the |
| * UIXCollection. |
| */ |
| protected boolean visitUnstampedFacets( |
| VisitContext visitContext, |
| VisitCallback callback) |
| { |
| // Visit the facets with no row |
| if (getFacetCount() > 0) |
| { |
| for (UIComponent facet : getFacets().values()) |
| { |
| if (UIXComponent.visitTree(visitContext, facet, callback)) |
| { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * VistiContext that visits the facets of the UIXColumn children, including |
| * nested UIXColumn childrem |
| */ |
| private static class ColumnFacetsOnlyVisitContext extends VisitContextWrapper |
| { |
| public ColumnFacetsOnlyVisitContext(VisitContext wrappedContext) |
| { |
| _wrapped = wrappedContext; |
| } |
| |
| @Override |
| public VisitContext getWrapped() |
| { |
| return _wrapped; |
| } |
| |
| @Override |
| public VisitResult invokeVisitCallback(UIComponent component, VisitCallback callback) |
| { |
| if (component instanceof UIXColumn) |
| { |
| if (component.getFacetCount() > 0) |
| { |
| // visit the facet children without filtering for just UIXColumn children |
| for (UIComponent facetChild : component.getFacets().values()) |
| { |
| if (UIXComponent.visitTree(getWrapped(), facetChild, callback)) |
| return VisitResult.COMPLETE; |
| } |
| |
| // visit the indexed children, recursively looking for more columns |
| for (UIComponent child : component.getChildren()) |
| { |
| if (UIXComponent.visitTree(this, child, callback)) |
| return VisitResult.COMPLETE; |
| } |
| } |
| } |
| |
| // at this point, we either have already manually processed the UIXColumn's children, or |
| // the component wasn't a UIXColumn and shouldn't be processed |
| return VisitResult.REJECT; |
| } |
| |
| private final VisitContext _wrapped; |
| } |
| |
| /** |
| * VisitContext implementation that doesn't visit any of the Facets of |
| * UIXColumn children. This is used when stamping children |
| */ |
| protected static final class NoColumnFacetsVisitContext extends VisitContextWrapper |
| { |
| NoColumnFacetsVisitContext(VisitContext wrapped) |
| { |
| _wrapped = wrapped; |
| } |
| |
| @Override |
| public VisitContext getWrapped() |
| { |
| return _wrapped; |
| } |
| |
| @Override |
| public VisitResult invokeVisitCallback(UIComponent component, VisitCallback callback) |
| { |
| if (component instanceof UIXColumn) |
| { |
| if (component.getChildCount() > 0) |
| { |
| // visit only the indexed children of the columns |
| for (UIComponent child : component.getChildren()) |
| { |
| if (UIXComponent.visitTree(this, child, callback)) |
| return VisitResult.COMPLETE; |
| } |
| } |
| |
| return VisitResult.REJECT; |
| } |
| else |
| { |
| // Components do not expect to be visited twice, in fact with UIXComponent, it is illegal. |
| // This is due to the fact that UIXComponent has setup and tearDown methods for visiting. |
| // In order to avoid having the setup method called for the current visit context and |
| // the wrapped context we invoke the visit on the component and then separately on the |
| // children of the component |
| VisitContext wrappedContext = getWrapped(); |
| VisitResult visitResult = wrappedContext.invokeVisitCallback(component, callback); |
| |
| if (visitResult == VisitResult.ACCEPT) |
| { |
| // Let the visitation continue with the wrapped context |
| return (UIXComponent.visitChildren(wrappedContext, component, callback)) ? |
| VisitResult.COMPLETE : VisitResult.REJECT; |
| } |
| else |
| { |
| return visitResult; |
| } |
| } |
| } |
| |
| private final VisitContext _wrapped; |
| } |
| |
| /** |
| * Implementation used to visit each stamped row |
| */ |
| private boolean _visitStampedColumnFacets( |
| VisitContext visitContext, |
| VisitCallback callback) |
| { |
| // visit the facets of the stamped columns |
| List<UIComponent> stamps = getStamps(); |
| |
| if (!stamps.isEmpty()) |
| { |
| VisitContext columnVisitingContext = new ColumnFacetsOnlyVisitContext(visitContext); |
| |
| for (UIComponent stamp : stamps) |
| { |
| if (UIXComponent.visitTree(columnVisitingContext, stamp, callback)) |
| { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Visit the rows and children of the columns of the collection per row-index. This should |
| * not visit row index -1 (it will be perfomed in the visitTree method). The columns |
| * themselves should not be visited, only their children in this function. |
| * |
| * @param visitContext The visiting context |
| * @param callback The visit callback |
| * @return true if the visiting should stop |
| * @see #visitChildren(VisitContext, VisitCallback) |
| */ |
| protected abstract boolean visitData( |
| VisitContext visitContext, |
| VisitCallback callback); |
| |
| /** |
| * Gets the CollectionModel to use with this component. |
| * |
| * @param createIfNull creates the collection model if necessary |
| */ |
| protected final CollectionModel getCollectionModel( |
| boolean createIfNull) |
| { |
| InternalState iState = _getInternalState(true); |
| if (iState._model == null && createIfNull) |
| { |
| // _init() is usually called from either processDecodes or encodeBegin. |
| // Sometimes both processDecodes and encodeBegin may not be called, |
| // but processSaveState is called (this happens when |
| // component's rendered attr is set to false). We need to make sure that |
| // _init() is called in that case as well. Otherwise we get nasty NPEs. |
| // safest place is to call it here: |
| _init(); |
| |
| iState._value = getValue(); |
| iState._model = createCollectionModel(null, iState._value); |
| postCreateCollectionModel(iState._model); |
| assert iState._model != null; |
| } |
| // model might not have been created if createIfNull is false: |
| if ((iState._initialStampStateKey == _NULL) && |
| (iState._model != null)) |
| { |
| // if we have not already initialized the initialStampStateKey |
| // that means that we don't have any stamp-state to use as the default |
| // state for rows that we have not seen yet. So... |
| // we will use any stamp-state for the initial rowKey on the model |
| // as the default stamp-state for all rows: |
| iState._initialStampStateKey = iState._model.getRowKey(); |
| } |
| return iState._model; |
| } |
| |
| /** |
| * Creates the CollectionModel to use with this component. |
| * The state of the UIComponent with the new model instance is not fully initialized until |
| * after this method returns. As a result, other component attributes that need |
| * a fully initialized model should not be initialized in this method. Instead, |
| * model-dependent initialization should be done in <code>postCreateCollectionModel</code> |
| * @see #postCreateCollectionModel |
| * @param current the current CollectionModel, or null if there is none. |
| * @param value this is the value returned from {@link #getValue()} |
| */ |
| protected abstract CollectionModel createCollectionModel( |
| CollectionModel current, |
| Object value); |
| |
| /** |
| * Hook called with the result of <code>createCollectionModel</code>. |
| * Subclasses can use this method to perform initialization after the CollectionModel |
| * is fully initialized. |
| * Subclassers should call super before accessing any component state to ensure |
| * that superclass initialization has been performed. |
| * @see #createCollectionModel |
| * @param model The model instance returned by<code><createCollectionModel</code> |
| */ |
| protected void postCreateCollectionModel(CollectionModel model) |
| { |
| // do nothing |
| } |
| |
| |
| /** |
| * Gets the value that must be converted into a CollectionModel |
| */ |
| protected abstract Object getValue(); |
| |
| /** |
| * Gets the Map to use as the "varStatus". |
| * This implementation supports the following keys:<ul> |
| * <li>model - returns the CollectionModel |
| * <li>index - returns the current rowIndex |
| * <li>rowKey - returns the current rowKey |
| * <li>current - returns the current rowData |
| * <li>"hierarchicalIndex" - returns an array containing zero based row index.</li> |
| * <li>"hierarchicalLabel" - returns a string label representing 1 based index of this row.</li> |
| * </ul> |
| */ |
| protected Map<String, Object> createVarStatusMap() |
| { |
| return new AbstractMap<String, Object>() |
| { |
| @Override |
| public Object get(Object key) |
| { |
| // some of these keys are from <c:forEach>, ie: |
| // javax.servlet.jsp.jstl.core.LoopTagStatus |
| if ("model".equals(key)) |
| return getCollectionModel(); |
| if ("rowKey".equals(key)) |
| return getRowKey(); |
| if ("index".equals(key)) // from jstl |
| return Integer.valueOf(getRowIndex()); |
| if("hierarchicalIndex".equals(key)) |
| { |
| int rowIndex = getRowIndex(); |
| return rowIndex>=0 ? new Integer[]{rowIndex}: new Integer[]{}; |
| } |
| if("hierarchicalLabel".equals(key)) |
| { |
| int rowIndex = getRowIndex(); |
| return rowIndex>=0 ? Integer.toString(rowIndex+1): ""; |
| } |
| if ("current".equals(key)) // from jstl |
| return getRowData(); |
| return null; |
| } |
| |
| @Override |
| public Set<Map.Entry<String, Object>> entrySet() |
| { |
| return Collections.emptySet(); |
| } |
| }; |
| } |
| |
| |
| // |
| // LocalRowKeyIndex implementation |
| // |
| |
| /** |
| * Given a row index, check if a row is locally available |
| * @param rowIndex index of row to check |
| * @return true if row is locally available |
| */ |
| public boolean isRowLocallyAvailable(int rowIndex) |
| { |
| return getCollectionModel().isRowLocallyAvailable(rowIndex); |
| } |
| |
| /** |
| * Given a row key, check if a row is locally available |
| * @param rowKey row key for the row to check |
| * @return true if row is locally available |
| */ |
| public boolean isRowLocallyAvailable(Object rowKey) |
| { |
| return getCollectionModel().isRowLocallyAvailable(rowKey); |
| } |
| |
| /** |
| * Check if a range of rows is locally available starting from current position |
| * @param rowCount number of rows in the range |
| * @return true if range of rows is locally available |
| */ |
| public boolean areRowsLocallyAvailable(int rowCount) |
| { |
| return getCollectionModel().areRowsLocallyAvailable(rowCount); |
| } |
| |
| /** |
| * Check if a range of rows is locally available starting from a row index |
| * @param startIndex staring index for the range |
| * @param rowCount number of rows in the range |
| * @return true if range of rows is locally available |
| */ |
| public boolean areRowsLocallyAvailable(int startIndex, int rowCount) |
| { |
| return getCollectionModel().areRowsLocallyAvailable(startIndex, rowCount); |
| } |
| |
| /** |
| * Check if a range of rows is locally available starting from a row key |
| * @param startRowKey staring row key for the range |
| * @param rowCount number of rows in the range |
| * @return true if range of rows is locally available |
| */ |
| public boolean areRowsLocallyAvailable(Object startRowKey, int rowCount) |
| { |
| return getCollectionModel().areRowsLocallyAvailable(startRowKey, rowCount); |
| } |
| |
| /** |
| * Convenient API to return a row count estimate. This method can be optimized |
| * to avoid a data fetch which may be required to return an exact row count |
| * @return estimated row count |
| */ |
| public int getEstimatedRowCount() |
| { |
| return getCollectionModel().getEstimatedRowCount(); |
| } |
| |
| |
| /** |
| * Helper API to determine if the row count returned from {@link #getEstimatedRowCount} |
| * is EXACT, or an ESTIMATE |
| */ |
| public LocalRowKeyIndex.Confidence getEstimatedRowCountConfidence() |
| { |
| return getCollectionModel().getEstimatedRowCountConfidence(); |
| } |
| |
| /** |
| * clear all rows from the local cache |
| */ |
| public void clearLocalCache() |
| { |
| getCollectionModel().clearLocalCache(); |
| } |
| |
| /** |
| * Clear the requested range of rows from the local cache |
| * @param startingIndex starting row index for the range to clear |
| * @param rowsToClear number of rows to clear from the cache |
| */ |
| public void clearCachedRows(int startingIndex, int rowsToClear) |
| { |
| getCollectionModel().clearCachedRows(startingIndex, rowsToClear); |
| } |
| |
| /** |
| * Clear the requested range of rows from the local cache |
| * @param startingRowKey starting row key for the range to clear |
| * @param rowsToClear number of rows to clear from the cache |
| */ |
| public void clearCachedRows(Object startingRowKey, int rowsToClear) |
| { |
| getCollectionModel().clearCachedRows(startingRowKey, rowsToClear); |
| } |
| |
| /** |
| * Clear a row from the local cache by row index |
| * @param index row index for the row to clear from the cache |
| */ |
| public void clearCachedRow(int index) |
| { |
| getCollectionModel().clearCachedRow(index); |
| } |
| |
| /** |
| * Clear a row from the local cache by row key |
| * @param rowKey row key for the row to clear from the cache |
| */ |
| public void clearCachedRow(Object rowKey) |
| { |
| getCollectionModel().clearCachedRow(rowKey); |
| } |
| |
| /** |
| * Indicates the caching strategy supported by the model |
| * @see LocalRowKeyIndex.LocalCachingStrategy |
| * @return caching strategy supported by the model |
| */ |
| public LocalRowKeyIndex.LocalCachingStrategy getCachingStrategy() |
| { |
| return getCollectionModel().getCachingStrategy(); |
| } |
| |
| /** |
| * Ensure that the model has at least rowCount number of rows. |
| * |
| * @param rowCount the number of rows the model should hold. |
| */ |
| public void ensureRowsAvailable(int rowCount) |
| { |
| getCollectionModel().ensureRowsAvailable(rowCount); |
| } |
| |
| /** |
| * override this method to place initialization code that must run |
| * once this component is created and the jsp engine has finished setting |
| * attributes on it. |
| */ |
| void __init() |
| { |
| InternalState iState = _getInternalState(true); |
| iState._var = getVar(); |
| if (_LOG.isFine() && (iState._var == null)) |
| { |
| _LOG.fine("'var' attribute is null."); |
| } |
| iState._varStatus = getVarStatus(); |
| if (_LOG.isFinest() && (iState._varStatus == null)) |
| { |
| _LOG.finest("'varStatus' attribute is null."); |
| } |
| } |
| |
| /** |
| * Hook for subclasses like UIXIterator to initialize and flush the cache when visting flattened |
| * children when parented by a renderer that needs to use |
| * UIXComponent.processFlattenedChildren(). |
| * This is to mimic what happens in the non flattening case where similar logic is invoked |
| * during encodeBegin(). |
| */ |
| protected void processFlattenedChildrenBegin(ComponentProcessingContext cpContext) |
| { |
| // Call _init() since __flushCachedModel() assumes that |
| // selectedRowKeys and disclosedRowKeys are initialized to be non-null. |
| _init(); |
| __flushCachedModel(); |
| } |
| |
| private void _init() |
| { |
| InternalState iState = _getInternalState(true); |
| if (!iState._isInitialized) |
| { |
| assert iState._model == null; |
| iState._isInitialized = true; |
| __init(); |
| } |
| } |
| |
| void __flushCachedModel() |
| { |
| InternalState iState = _getInternalState(true); |
| Object value = getValue(); |
| if (iState._value != value) |
| { |
| CollectionModel oldModel = iState._model; |
| iState._value = value; |
| iState._model = createCollectionModel(iState._model, value); |
| postCreateCollectionModel(iState._model); |
| |
| // if the underlying model is changed, we need to remove |
| // the listener from the old model. And if we still have cached |
| // rowkey, we need to add the listener back to the new model. |
| |
| if (oldModel != iState._model) |
| { |
| if (oldModel != null) |
| { |
| oldModel.removeRowKeyChangeListener(iState); |
| } |
| |
| if (iState._currentRowKey != _NULL) |
| { |
| iState._model.addRowKeyChangeListener(iState); |
| } |
| } |
| } |
| } |
| |
| // |
| // Returns true if this is the first request to invokeOnComponent() |
| // |
| static private boolean _getAndMarkFirstInvokeForRequest( |
| FacesContext context, String clientId) |
| { |
| // See if the request contains a marker that we've hit this |
| // method already for this clientId |
| Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); |
| String key = _INVOKE_KEY + clientId; |
| // Yep, we have, so return true |
| if (requestMap.containsKey(key)) |
| return true; |
| |
| // Stash TRUE for next time, and return false |
| requestMap.put(key, Boolean.TRUE); |
| return false; |
| } |
| |
| /** |
| * Gets the internal state of this component. |
| * This is to support table within table. |
| */ |
| Object __getMyStampState() |
| { |
| return _state; |
| } |
| |
| /** |
| * Sets the internal state of this component. |
| * This is to support table within table. |
| * @param stampState the internal state is obtained from this object. |
| */ |
| void __setMyStampState(Object stampState) |
| { |
| InternalState iState = (InternalState) stampState; |
| _state = iState; |
| } |
| |
| /** |
| * reset the stamp state to pristine state. This pristine state when saved to the outer collection for null currency |
| * will allow stamp state for UIXCollection with individual rows to be created |
| * |
| * This is to support iteration of children(column stamping) within the table. |
| */ |
| void __resetMyStampState() |
| { |
| _state = null; |
| } |
| |
| /** |
| * Returns true if an event (other than a selection event) |
| * has been queued for this component. This is a hack |
| * to support validation in the tableSelectXyz components. |
| */ |
| boolean __hasEvent() |
| { |
| InternalState iState = _getInternalState(true); |
| return iState._hasEvent; |
| } |
| |
| /** |
| * Saves the state of all the stamps of this component. |
| * This method should be called before the rowData of this component |
| * changes. This method gets all the stamps using {@link #getStamps} and |
| * saves their states by calling {@link #saveStampState}. |
| */ |
| private void _saveStampState() |
| { |
| // Never read and created by _getStampState |
| //InternalState iState = _getInternalState(true); |
| |
| StampState stampState = _getStampState(); |
| Map<String, String> idToIndexMap = _getIdToIndexMap(); |
| FacesContext context = getFacesContext(); |
| Object currencyObj = getRowKey(); |
| |
| // Note: even though the currencyObj may be null, we still need to save the state. The reason |
| // is that the code does not clear out the state when it is saved, instead, the un-stamped |
| // state is saved. Once the row key is set back to null, this un-stamped state is restored |
| // onto the children components. This restoration allows editable value holders, show detail |
| // items and nested UIXCollections to clear their state. |
| // For nested UIXCollections, this un-stamped state is required to set the nested collection's |
| // _state (internal state containing the stamp state) to null when not on a row key. Without |
| // that call, the nested UIXCollection components would end up sharing the same stamp state |
| // across parent rows. |
| |
| for (UIComponent stamp : getStamps()) |
| { |
| Object state = saveStampState(context, stamp); |
| // String stampId = stamp.getId(); |
| // TODO |
| // temporarily use position. later we need to use ID's to access |
| // stamp state everywhere, and special case NamingContainers: |
| String compId = stamp.getId(); |
| String stampId = idToIndexMap.get(compId); |
| if (stampId == null) |
| { |
| stampId = String.valueOf(idToIndexMap.size()); |
| idToIndexMap.put(compId, stampId); |
| } |
| stampState.put(currencyObj, stampId, state); |
| if (_LOG.isFinest()) |
| _LOG.finest("saving stamp state for currencyObject:"+currencyObj+ |
| " and stampId:"+stampId); |
| } |
| } |
| |
| |
| /** |
| * Restores the state of all the stamps of this component. |
| * This method should be called after the currency of this component |
| * changes. This method gets all the stamps using {@link #getStamps} and |
| * restores their states by calling |
| * {@link #restoreStampState}. |
| */ |
| private void _restoreStampState() |
| { |
| StampState stampState = _getStampState(); |
| Map<String, String> idToIndexMap = _getIdToIndexMap(); |
| FacesContext context = getFacesContext(); |
| Object currencyObj = getRowKey(); |
| for(UIComponent stamp : getStamps()) |
| { |
| // String stampId = stamp.getId(); |
| // TODO |
| // temporarily use position. later we need to use ID's to access |
| // stamp state everywhere, and special case NamingContainers: |
| String compId = stamp.getId(); |
| String stampId = idToIndexMap.get(compId); |
| Object state = stampState.get(currencyObj, stampId); |
| if (state == null) |
| { |
| Object iniStateObj = _getCurrencyKeyForInitialStampState(); |
| state = stampState.get(iniStateObj, stampId); |
| /* |
| if (state==null) |
| { |
| _LOG.severe("NO_INITIAL_STAMP_STATE", new Object[]{currencyObj,iniStateObj,stampId}); |
| continue; |
| }*/ |
| } |
| restoreStampState(context, stamp, state); |
| } |
| } |
| |
| private Map<String, String> _getIdToIndexMap() |
| { |
| InternalState iState = _getInternalState(true); |
| if (iState._idToIndexMap == null) |
| iState._idToIndexMap = new HashMap<String, String>(); |
| return iState._idToIndexMap; |
| } |
| |
| private InternalState _getInternalState(boolean create) |
| { |
| if ((_state == null) && create) |
| { |
| _state = new InternalState(); |
| } |
| return _state; |
| } |
| |
| private StampState _getStampState() |
| { |
| InternalState iState = _getInternalState(true); |
| if (iState._stampState == null) |
| iState._stampState = new StampState(); |
| |
| return iState._stampState; |
| } |
| |
| /** |
| * sets an EL variable. |
| * @param varName the name of the variable |
| * @param newData the value of the variable |
| * @return the old value of the variable, or null if there was no old value. |
| */ |
| private Object _setELVar(String varName, Object newData) |
| { |
| if (varName == null) |
| return null; |
| |
| // we need to place each row at an EL reachable place so that it |
| // can be accessed via the 'var' variable. Let's place it on the |
| // requestMap: |
| return setupELVariable(getFacesContext(), varName, newData); |
| } |
| |
| /** |
| * Called by UIXCollection to set values for the "var" and |
| * "varStatus" EL variables. |
| * |
| * @param context the FacesContext for the current request |
| * @param name the non-null name of the EL variable |
| * @param value the value of the EL variable |
| * @return the previous value of the EL variable, or null if |
| * the value was not previously set. |
| */ |
| protected Object setupELVariable( |
| FacesContext context, |
| String name, |
| Object value |
| ) |
| { |
| Args.notNull(name, "name"); |
| return TableUtils.setupELVariable(context, name, value); |
| } |
| |
| private static boolean _equals(Object a, Object b) |
| { |
| if (b == null) |
| return (a == null); |
| |
| return b.equals(a); |
| } |
| |
| private void _setupContextChange() |
| { |
| if (_inSuspendOrResume) |
| { |
| // This situation will occur when the CollectionComponentChange is currently setting the |
| // row key. |
| return; |
| } |
| |
| ComponentContextManager compCtxMgr = |
| RequestContext.getCurrentInstance().getComponentContextManager(); |
| |
| compCtxMgr.pushChange(new CollectionComponentChange(this)); |
| |
| _inContext = true; |
| } |
| |
| private void _tearDownContextChange() |
| { |
| if (_inSuspendOrResume) |
| { |
| // This situation will occur when the CollectionComponentChange is currently setting the |
| // row key. |
| return; |
| } |
| |
| try |
| { |
| ComponentContextManager compCtxMgr = |
| RequestContext.getCurrentInstance().getComponentContextManager(); |
| ComponentContextChange change = compCtxMgr.peekChange(); |
| |
| if (change instanceof CollectionComponentChange && |
| ((CollectionComponentChange)change)._component == this) |
| { |
| // Remove the component context change if one was added |
| compCtxMgr.popChange(); |
| } |
| else |
| { |
| _LOG.severe("COLLECTION_CHANGE_TEARDOWN", new Object[] { getId(), change }); |
| } |
| } |
| catch (RuntimeException re) |
| { |
| _LOG.severe(re); |
| } |
| finally |
| { |
| _inContext = false; |
| } |
| } |
| |
| private void _verifyComponentInContext() |
| { |
| if (_inSuspendOrResume) |
| { |
| return; |
| } |
| |
| if (!_inContext) |
| { |
| if (_LOG.isWarning()) |
| { |
| _LOG.warning("COLLECTION_NOT_IN_CONTEXT", |
| new Object[] { getParent() == null ? getId() : getClientId() }); |
| if (_LOG.isFine()) |
| { |
| Thread.currentThread().dumpStack(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * during state saving, we want to reset the currency to null, but we want to |
| * remember the current currency, so that after state saving, we can set it back |
| * |
| * @param context faces context |
| * @return the currency key |
| */ |
| private Object _resetCurrencyKeyForStateSaving(FacesContext context) |
| { |
| // If we saved state in the middle of processing a row, |
| // then make sure that we revert to a "null" rowKey while |
| // saving state; this is necessary to ensure that the |
| // current row's state is properly preserved, and that |
| // the children are reset to their default state. |
| Object currencyKey = _getCurrencyKey(); |
| |
| // since this is the end of the request, we expect the row currency to be reset back to null |
| // setting it and leaving it there might introduce multiple issues, so log a warning here |
| if (currencyKey != null) |
| { |
| if (_LOG.isWarning()) |
| { |
| String scopedId = ComponentUtils.getScopedIdForComponent(this, context.getViewRoot()); |
| String viewId = context.getViewRoot() == null? null: context.getViewRoot().getViewId(); |
| _LOG.warning("ROWKEY_NOT_RESET", new Object[] |
| { scopedId, viewId }); |
| } |
| } |
| |
| Object initKey = _getCurrencyKeyForInitialStampState(); |
| if (currencyKey != initKey) // beware of null currencyKeys if equals() is used |
| { |
| setRowKey(initKey); |
| } |
| |
| return currencyKey; |
| } |
| |
| /** |
| * restore the currency key after state saving |
| * |
| * @param key the currency key |
| */ |
| private void _restoreCurrencyKeyForStateSaving(Object key) |
| { |
| Object currencyKey = key; |
| Object initKey = _getCurrencyKeyForInitialStampState(); |
| |
| if (currencyKey != initKey) // beware of null currencyKeys if equals() is used |
| { |
| setRowKey(currencyKey); |
| } |
| } |
| |
| /** |
| * clean up any internal model state that we might be holding on to. |
| */ |
| private void _resetInternalState() |
| { |
| InternalState iState = _getInternalState(false); |
| if (iState != null) |
| { |
| iState._value = null; |
| |
| if (iState._model != null) |
| { |
| iState._model.removeRowKeyChangeListener(iState); |
| iState._model = null; |
| } |
| } |
| } |
| |
| private static final class DefaultClientKeyManager extends ClientRowKeyManager |
| { |
| public void clear() |
| { |
| _currencyCache.clear(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Object getRowKey(FacesContext context, UIComponent component, String clientRowKey) |
| { |
| ValueMap<Object,String> currencyCache = _currencyCache; |
| Object rowkey = currencyCache.getKey(clientRowKey); |
| return rowkey; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getClientRowKey(FacesContext context, UIComponent component, Object rowKey) |
| { |
| assert rowKey != null; |
| |
| ValueMap<Object,String> currencyCache = _currencyCache; |
| String key = currencyCache.get(rowKey); |
| // check to see if we already have a string key: |
| if (key == null) |
| { |
| // we don't have a string-key, so create a new one. |
| key = _createToken(currencyCache); |
| |
| if (_LOG.isFiner()) |
| _LOG.finer("Storing token:"+key+ |
| " for rowKey:"+rowKey); |
| |
| currencyCache.put(rowKey, key); |
| } |
| return key; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean replaceRowKey(FacesContext context, UIComponent component, Object oldRowKey, Object newRowKey) |
| { |
| assert oldRowKey != null && newRowKey != null; |
| |
| ValueMap<Object,String> currencyCache = _currencyCache; |
| String key = currencyCache.remove(oldRowKey); |
| // check to see if we already have a string key: |
| if (key != null) |
| { |
| currencyCache.put(newRowKey, key); |
| } |
| return key != null; |
| } |
| |
| |
| |
| private static String _createToken(ValueMap<Object,String> currencyCache) |
| { |
| String key = String.valueOf(currencyCache.size()); |
| return key; |
| } |
| |
| private ValueMap<Object,String> _currencyCache = new ValueMap<Object,String>(); |
| private static final long serialVersionUID = 1L; |
| } |
| |
| // this component's internal state is stored in an inner class |
| // rather than in individual fields, because we want to make it |
| // easy to quickly suck out or restore its internal state, |
| // when this component is itself used as a stamp inside some other |
| // stamping container, eg: nested tables. |
| private static final class InternalState implements RowKeyChangeListener, Serializable |
| { |
| private transient boolean _hasEvent = false; |
| private transient Object _prevVarValue = _NULL; |
| private transient Object _prevVarStatus = _NULL; |
| private transient String _var = null; |
| private transient String _varStatus = null; |
| private transient Object _value = null; |
| private transient CollectionModel _model = null; |
| private transient Object _currentRowKey = _NULL; |
| private transient boolean _clearTokenCache = false; |
| // this is true if this is the first request for this viewID and processDecodes |
| // was not called: |
| private transient boolean _isFirstRender = true; |
| private transient boolean _isInitialized = false; |
| // this is the rowKey used to retrieve the default stamp-state for all rows: |
| private transient Object _initialStampStateKey = _NULL; |
| |
| private ClientRowKeyManager _clientKeyMgr = null; |
| private StampState _stampState = null; |
| |
| // map from column id to the index within the collection. The index is used to |
| // save/look up for column's stamp state. The long term goal is to use id as key |
| // to the stamp state map, but for this release, we add this id-to-index map |
| // to indirectly loop up a stamp state for a column, so if the position of the column |
| // changes in the middle, we'll still be able to find the right stamp state. |
| private Map<String, String> _idToIndexMap = null; |
| |
| public void onRowKeyChange(RowKeyChangeEvent rowKeyChangeEvent) |
| { |
| Object newKey = rowKeyChangeEvent.getNewRowKey(); |
| Object oldKey = rowKeyChangeEvent.getOldRowKey(); |
| |
| if (newKey != null && oldKey != null && !newKey.equals(oldKey)) |
| { |
| // first replace the cached row key |
| if (oldKey.equals(_currentRowKey)) |
| _currentRowKey = newKey; |
| |
| // then update stamp state for the affected entries. |
| if (_stampState == null || _idToIndexMap == null) |
| return; |
| |
| int stampCompCount = _idToIndexMap.size(); |
| |
| for (int index = 0; index < stampCompCount; index++) |
| { |
| String stampId = String.valueOf(index); |
| Object state = _stampState.get(oldKey, stampId); |
| if (state == null) |
| continue; |
| _stampState.put(oldKey, stampId, null); |
| _stampState.put(newKey, stampId, state); |
| } |
| } |
| } |
| |
| private void readObject(ObjectInputStream in) |
| throws IOException, ClassNotFoundException |
| { |
| in.defaultReadObject(); |
| // Set values of all transients to their defaults |
| _prevVarValue = _NULL; |
| _prevVarStatus = _NULL; |
| _currentRowKey = _NULL; |
| _initialStampStateKey = _NULL; |
| // However, leave _isFirstRender set to false - since that's |
| // necessarily the state we'd be in if we're reconstituting this |
| _isFirstRender = false; |
| } |
| |
| private static final long serialVersionUID = 1L; |
| } |
| |
| /** |
| * Class to be able to suspend the context of the collection. |
| * <p>Current implementation removes the var and varStatus from the request while the |
| * collection is suspended.</p> |
| */ |
| private static class CollectionComponentChange |
| extends ComponentContextChange |
| { |
| private CollectionComponentChange( |
| UIXCollection component) |
| { |
| _component = component; |
| } |
| |
| public void suspend( |
| FacesContext facesContext) |
| { |
| _component._inSuspendOrResume = true; |
| |
| try |
| { |
| InternalState iState = _component._getInternalState(false); |
| if (iState == null || iState._model == null || iState._currentRowKey == _NULL) |
| { |
| // If we were to try to call getRowKey() here, this would call getCollectionModel(). |
| // The get collection model may result in EL being evaluated, which is undesirable |
| // and will cause bugs when called while we are suspending. This is because evaluating |
| // EL may need to suspend or resume other component context changes, and we do not want |
| // re-entrant calls to the component context stack while we are already suspending. |
| |
| // Note that this code will fail if someone has set the _model to null while on a rowKey |
| // (Should not happen, would be considered a bug if that were to be done). |
| _rowKey = null; |
| } |
| else |
| { |
| _rowKey = _component.getRowKey(); |
| |
| // Set the row key back to null to force the collection into the un-stamped state. This |
| // will ensure that the collection is not in a row key while the component context is |
| // not setup. Only do this if the row key is not already on the null row key. |
| if (_rowKey != null) |
| { |
| _component.setRowKey(null); |
| } |
| } |
| |
| _component._inContext = false; |
| } |
| finally |
| { |
| _component._inSuspendOrResume = false; |
| } |
| } |
| |
| public void resume( |
| FacesContext facesContext) |
| { |
| _component._inSuspendOrResume = true; |
| try |
| { |
| // Only set the row key if one was stored during the suspend. |
| if (_rowKey != null) |
| { |
| _component.setRowKey(_rowKey); |
| } |
| |
| _component._inContext = true; |
| } |
| finally |
| { |
| _component._inSuspendOrResume = false; |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| String className = _component.getClass().getName(); |
| String componentId = _component.getId(); |
| return new StringBuilder(58 + className.length() + componentId.length()) |
| .append("UIXCollection.CollectionComponentChange[Component class: ") |
| .append(className) |
| .append(", component ID: ") |
| .append(componentId) |
| .append("]") |
| .toString(); |
| } |
| |
| private final UIXCollection _component; |
| private CollectionModel _collectionModel; |
| private Object _rowKey; |
| } |
| |
| private static class CollectionContextEvent |
| extends WrapperEvent |
| { |
| public CollectionContextEvent( |
| UIComponent source, |
| FacesEvent event) |
| { |
| super(source, event); |
| } |
| |
| @SuppressWarnings("compatibility:-7639429485707197863") |
| private static final long serialVersionUID = 1L; |
| } |
| |
| // do not assign a non-null value. values should be assigned lazily. this is |
| // because this value is preserved by stampStateSaving, when table-within-table |
| // is used. And if a non-null value is used, then all nested tables will |
| // end up sharing this stampState. see bug 4279735: |
| private InternalState _state = null; |
| private boolean _inSuspendOrResume = false; |
| private boolean _inContext = false; |
| |
| // use this key to indicate uninitialized state. |
| // all the variables that use this are transient so this object need not |
| // be Serializable: |
| private static final Object _NULL = new Object(); |
| private static final String _INVOKE_KEY = |
| UIXCollection.class.getName() + ".INVOKE"; |
| |
| private transient Object _stateSavingCurrencyKey = null; |
| |
| private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(UIXCollection.class); |
| |
| // An enum to throw into state-saving so that we get a nice |
| // instance-equality to test against for noticing transient components |
| // (and better serialization results) |
| // We need this instead of just using null - because transient components |
| // are specially handled, since they may or may not actually still |
| // be there when you go to restore state later (e.g., on the next request!) |
| enum Transient { TRUE }; |
| } |