jakarta.faces.component.UIData Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.component;
import java.io.IOException;
import java.io.Serializable;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import jakarta.el.ValueExpression;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.faces.FacesException;
import jakarta.faces.application.Application;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.application.StateManager;
import jakarta.faces.component.visit.VisitCallback;
import jakarta.faces.component.visit.VisitContext;
import jakarta.faces.component.visit.VisitHint;
import jakarta.faces.component.visit.VisitResult;
import jakarta.faces.context.FacesContext;
import jakarta.faces.el.ValueBinding;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.FacesEvent;
import jakarta.faces.event.FacesListener;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PostValidateEvent;
import jakarta.faces.event.PreValidateEvent;
import jakarta.faces.model.ArrayDataModel;
import jakarta.faces.model.CollectionDataModel;
import jakarta.faces.model.DataModel;
import jakarta.faces.model.FacesDataModel;
import jakarta.faces.model.IterableDataModel;
import jakarta.faces.model.ListDataModel;
import jakarta.faces.model.ResultDataModel;
import jakarta.faces.model.ResultSetDataModel;
import jakarta.faces.model.ScalarDataModel;
import jakarta.servlet.jsp.jstl.sql.Result;
// ------------------------------------------------------------- Private Classes
// Private class to represent saved state information
/**
*
* UIData is a
* {@link UIComponent} that supports data binding to a collection of data objects represented by a {@link DataModel}
* instance, which is the current value of this component itself (typically established via a {@link ValueExpression}).
* During iterative processing over the rows of data in the data model, the object for the current row is exposed as a
* request attribute under the key specified by the var
property.
*
*
* Only children of type {@link UIColumn} should be processed by renderers associated with this component.
*
*
* By default, the rendererType
property is set to jakarta.faces.Table
. This value can be
* changed by calling the setRendererType()
method.
*
*/
public class UIData extends UIComponentBase implements NamingContainer, UniqueIdVendor {
// ------------------------------------------------------ Manifest Constants
/**
*
* The standard component type for this component.
*
*/
public static final String COMPONENT_TYPE = "jakarta.faces.Data";
/**
*
* The standard component family for this component.
*
*/
public static final String COMPONENT_FAMILY = "jakarta.faces.Data";
// ------------------------------------------------------------ Constructors
/**
*
* Create a new {@link UIData} instance with default property values.
*
*/
public UIData() {
super();
setRendererType("jakarta.faces.Table");
}
// ------------------------------------------------------ Instance Variables
/**
* Properties that are tracked by state saving.
*/
enum PropertyKeys {
/**
*
* The first row number (zero-relative) to be displayed.
*
*/
first,
/**
*
* The zero-relative index of the current row number, or -1 for no current row association.
*
*/
rowIndex,
/**
*
* The number of rows to display, or zero for all remaining rows in the table.
*
*/
rows,
/**
*
* This map contains SavedState
instances for each descendant component, keyed by the client identifier of
* the descendant. Because descendant client identifiers will contain the rowIndex
value of the parent,
* per-row state information is actually preserved.
*
*/
saved,
/**
*
* The local value of this {@link UIComponent}.
*
*/
value,
/**
*
* The request scope attribute under which the data object for the current row will be exposed when iterating.
*
*/
var,
/**
*
* Last id vended by {@link UIData#createUniqueId(jakarta.faces.context.FacesContext, String)}.
*
*/
lastId,
/**
*
*/
rowStatePreserved
}
/**
*
* The {@link DataModel} associated with this component, lazily instantiated if requested. This object is not part of
* the saved and restored state of the component.
*
*/
private DataModel model = null;
/**
*
* During iteration through the rows of this table, This ivar is used to store the previous "var" value for this
* instance. When the row iteration is complete, this value is restored to the request map.
*/
private Object oldVar;
/**
*
* Holds the base client ID that will be used to generate per-row client IDs (this will be null if this UIData is nested
* within another).
*
*
*
* This is not part of the component state.
*
*/
private String baseClientId = null;
/**
*
* Length of the cached baseClientId
plus one for the {@link UINamingContainer#getSeparatorChar}.
*
*
*
* This is not part of the component state.
*
*/
private int baseClientIdLength;
/**
*
* StringBuilder used to build per-row client IDs.
*
*
*
* This is not part of the component state.
*
*/
private StringBuilder clientIdBuilder = null;
/**
*
* Flag indicating whether or not this UIData instance is nested within another UIData instance
*
*
*
* This is not part of the component state.
*
*/
private Boolean isNested = null;
private Map _rowDeltaStates = new HashMap<>();
private Map _rowTransientStates = new HashMap<>();
private Object _initialDescendantFullComponentState = null;
// -------------------------------------------------------------- Properties
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
/**
*
* Return the zero-relative row number of the first row to be displayed.
*
*
* @return the row number.
*/
public int getFirst() {
return (Integer) getStateHelper().eval(PropertyKeys.first, 0);
}
/**
*
* Set the zero-relative row number of the first row to be displayed.
*
*
* @param first New first row number
*
* @throws IllegalArgumentException if first
is negative
*/
public void setFirst(int first) {
if (first < 0) {
throw new IllegalArgumentException(String.valueOf(first));
}
getStateHelper().put(PropertyKeys.first, first);
}
/**
*
* Return the footer facet of this component (if any). A convenience method for getFacet("footer")
.
*
*
* @return the footer facet.
*/
public UIComponent getFooter() {
return getFacet("footer");
}
/**
*
* Set the footer facet of this component. A convenience method for getFacets().put("footer", footer)
.
*
*
* @param footer the new footer facet
*
* @throws NullPointerException if footer
is null
*/
public void setFooter(UIComponent footer) {
getFacets().put("footer", footer);
}
/**
*
* Return the header facet of this component (if any). A convenience method for getFacet("header")
.
*
*
* @return the header facet.
*/
public UIComponent getHeader() {
return getFacet("header");
}
/**
*
* Set the header facet of this component. A convenience method for getFacets().put("header", header)
.
*
*
* @param header the new header facet
*
* @throws NullPointerException if header
is null
*/
public void setHeader(UIComponent header) {
getFacets().put("header", header);
}
/**
*
* Return a flag indicating whether there is rowData
available at the current rowIndex
. If no
* wrappedData
is available, return false
.
*
*
* @return whether the row is available.
*
* @throws FacesException if an error occurs getting the row availability
*/
public boolean isRowAvailable() {
return getDataModel().isRowAvailable();
}
/**
*
* Return the number of rows in the underlying data model. If the number of available rows is unknown, return -1.
*
*
* @return the row count.
* @throws FacesException if an error occurs getting the row count
*/
public int getRowCount() {
return getDataModel().getRowCount();
}
/**
*
* Return the data object representing the data for the currently selected row index, if any.
*
*
* @return the row data.
*
* @throws FacesException if an error occurs getting the row data
* @throws IllegalArgumentException if now row data is available at the currently specified row index
*/
public Object getRowData() {
return getDataModel().getRowData();
}
/**
*
* Return the zero-relative index of the currently selected row. If we are not currently positioned on a row, return -1.
* This property is not enabled for value binding expressions.
*
*
* @return the row index.
*
* @throws FacesException if an error occurs getting the row index
*/
public int getRowIndex() {
return (Integer) getStateHelper().eval(PropertyKeys.rowIndex, -1);
}
/**
*
* Set the zero relative index of the current row, or -1 to indicate that no
* row is currently selected, by implementing the following algorithm. It is possible to set the row index at a value
* for which the underlying data collection does not contain any row data. Therefore, callers may use the
* isRowAvailable()
method to detect whether row data will be available for use by the
* getRowData()
method.
*
*
*
* To support transient state among descendents, please consult the specification for {@link #setRowStatePreserved},
* which details the requirements for setRowIndex()
when the rowStatePreserved
JavaBeans
* property is set to true
.
*
*
*
* - Save current state information for all descendant components (as described below).
*
- Store the new row index, and pass it on to the {@link DataModel} associated with this {@link UIData}
* instance.
* - If the new
rowIndex
value is -1:
*
* - If the
var
property is not null, remove the corresponding request scope attribute (if any).
* - Reset the state information for all descendant components (as described below).
*
*
* - If the new
rowIndex
value is not -1:
*
* - If the
var
property is not null, call getRowData()
and expose the resulting data object
* as a request scope attribute whose key is the var
property value.
* - Reset the state information for all descendant components (as described below).
*
*
*
*
*
* To save current state information for all descendant components, {@link UIData} must maintain per-row information for
* each descendant as follows:
*
*
* - If the descendant is an instance of
EditableValueHolder
, save the state of its
* localValue
property.
* - If the descendant is an instance of
EditableValueHolder
, save the state of the
* localValueSet
property.
* - If the descendant is an instance of
EditableValueHolder
, save the state of the valid
* property.
* - If the descendant is an instance of
EditableValueHolder
, save the state of the
* submittedValue
property.
*
*
*
* To restore current state information for all descendant components, {@link UIData} must reference its previously
* stored information for the current rowIndex
and call setters for each descendant as follows:
*
*
* - If the descendant is an instance of
EditableValueHolder
, restore the value
* property.
* - If the descendant is an instance of
EditableValueHolder
, restore the state of the
* localValueSet
property.
* - If the descendant is an instance of
EditableValueHolder
, restore the state of the valid
* property.
* - If the descendant is an instance of
EditableValueHolder
, restore the state of the
* submittedValue
property.
*
*
* @param rowIndex The new row index value, or -1 for no associated row
*
* @throws FacesException if an error occurs setting the row index
* @throws IllegalArgumentException if rowIndex
is less than -1
*/
public void setRowIndex(int rowIndex) {
if (isRowStatePreserved()) {
setRowIndexRowStatePreserved(rowIndex);
} else {
setRowIndexWithoutRowStatePreserved(rowIndex);
}
}
private void setRowIndexWithoutRowStatePreserved(int rowIndex) {
// Save current state for the previous row index
saveDescendantState();
// Update to the new row index
// this.rowIndex = rowIndex;
getStateHelper().put(PropertyKeys.rowIndex, rowIndex);
DataModel localModel = getDataModel();
localModel.setRowIndex(rowIndex);
// if rowIndex is -1, clear the cache
if (rowIndex == -1) {
setDataModel(null);
}
// Clear or expose the current row data as a request scope attribute
String var = (String) getStateHelper().get(PropertyKeys.var);
if (var != null) {
Map requestMap = getFacesContext().getExternalContext().getRequestMap();
if (rowIndex == -1) {
oldVar = requestMap.remove(var);
} else if (isRowAvailable()) {
requestMap.put(var, getRowData());
} else {
requestMap.remove(var);
if (null != oldVar) {
requestMap.put(var, oldVar);
oldVar = null;
}
}
}
// Reset current state information for the new row index
restoreDescendantState();
}
private void setRowIndexRowStatePreserved(int rowIndex) {
if (rowIndex < -1) {
throw new IllegalArgumentException("rowIndex is less than -1");
}
if (getRowIndex() == rowIndex) {
return;
}
FacesContext facesContext = getFacesContext();
if (_initialDescendantFullComponentState != null) {
// Just save the row
Map sm = saveFullDescendantComponentStates(facesContext, null, getChildren().iterator(), false);
if (sm != null && !sm.isEmpty()) {
_rowDeltaStates.put(getContainerClientId(facesContext), sm);
}
if (getRowIndex() != -1) {
_rowTransientStates.put(getContainerClientId(facesContext),
saveTransientDescendantComponentStates(facesContext, null, getChildren().iterator(), false));
}
}
// Update to the new row index
// this.rowIndex = rowIndex;
getStateHelper().put(PropertyKeys.rowIndex, rowIndex);
DataModel localModel = getDataModel();
localModel.setRowIndex(rowIndex);
// if rowIndex is -1, clear the cache
if (rowIndex == -1) {
setDataModel(null);
}
// Clear or expose the current row data as a request scope attribute
String var = (String) getStateHelper().get(PropertyKeys.var);
if (var != null) {
Map requestMap = getFacesContext().getExternalContext().getRequestMap();
if (rowIndex == -1) {
oldVar = requestMap.remove(var);
} else if (isRowAvailable()) {
requestMap.put(var, getRowData());
} else {
requestMap.remove(var);
if (null != oldVar) {
requestMap.put(var, oldVar);
oldVar = null;
}
}
}
if (_initialDescendantFullComponentState != null) {
Object rowState = _rowDeltaStates.get(getContainerClientId(facesContext));
if (rowState == null) {
// Restore as original
restoreFullDescendantComponentStates(facesContext, getChildren().iterator(), _initialDescendantFullComponentState, false);
} else {
// Restore first original and then delta
restoreFullDescendantComponentDeltaStates(facesContext, getChildren().iterator(), rowState, _initialDescendantFullComponentState, false);
}
if (getRowIndex() == -1) {
restoreTransientDescendantComponentStates(facesContext, getChildren().iterator(), null, false);
} else {
rowState = _rowTransientStates.get(getContainerClientId(facesContext));
if (rowState == null) {
restoreTransientDescendantComponentStates(facesContext, getChildren().iterator(), null, false);
} else {
restoreTransientDescendantComponentStates(facesContext, getChildren().iterator(), (Map) rowState, false);
}
}
}
}
/**
*
* Return the number of rows to be displayed, or zero for all remaining rows in the table. The default value of this
* property is zero.
*
*
* @return the number of rows.
*/
public int getRows() {
return (Integer) getStateHelper().eval(PropertyKeys.rows, 0);
}
/**
*
* Set the number of rows to be displayed, or zero for all remaining rows in the table.
*
*
* @param rows New number of rows
*
* @throws IllegalArgumentException if rows
is negative
*/
public void setRows(int rows) {
if (rows < 0) {
throw new IllegalArgumentException(String.valueOf(rows));
}
getStateHelper().put(PropertyKeys.rows, rows);
}
/**
*
* Return the request-scope attribute under which the data object for the current row will be exposed when iterating.
* This property is not enabled for value binding expressions.
*
*
* @return he request-scope attribute.
*/
public String getVar() {
return (String) getStateHelper().get(PropertyKeys.var);
}
/**
*
* Set the request-scope attribute under which the data object for the current row wil be exposed when iterating.
*
*
* @param var The new request-scope attribute name
*/
public void setVar(String var) {
getStateHelper().put(PropertyKeys.var, var);
}
/**
*
* Return the value of the rowStatePreserved
JavaBeans property. See {@link #setRowStatePreserved}.
*
*
* @return the value of the rowStatePreserved
.
*
* @since 2.1
*/
public boolean isRowStatePreserved() {
Boolean b = (Boolean) getStateHelper().get(PropertyKeys.rowStatePreserved);
return b == null ? false : b.booleanValue();
}
/**
*
* If this property is set to true
, the UIData
must take steps to ensure that modifications to
* its iterated children will be preserved on a per-row basis. This allows applications to modify component properties,
* such as the style-class, for a specific row, rather than having such modifications apply to all rows.
*
*
*
*
*
* To accomplish this, UIData
must call {@link StateHolder#saveState} and
* {@link TransientStateHolder#saveTransientState} on its children to capture their state on exiting each row. When
* re-entering the row, {@link StateHolder#restoreState} and {@link TransientStateHolder#restoreTransientState} must be
* called in order to reinitialize the children to the correct state for the new row. All of this action must take place
* during the processing of {@link #setRowIndex}.
*
*
*
* Users should consider enabling this feature for cases where it is necessary to modify properties of
* UIData
's children in a row-specific way. Note, however, that row-level state saving/restoring does add
* overhead. As such, this feature should be used judiciously.
*
*
*
*
* @param preserveComponentState the flag if the state should be preserved.
*
* @since 2.1
*/
public void setRowStatePreserved(boolean preserveComponentState) {
getStateHelper().put(PropertyKeys.rowStatePreserved, preserveComponentState);
}
// ----------------------------------------------------- StateHolder Methods
/**
*
* Return the value of the UIData. This value must either be be of type
* {@link DataModel}, or a type that can be adapted into a {@link DataModel}. UIData
will automatically
* adapt the following types:
*
*
* - Arrays
* java.util.List
* java.sql.ResultSet
* jakarta.servlet.jsp.jstl.sql.Result
* java.util.Collection
*
*
* All other types will be adapted using the {@link ScalarDataModel} class, which will treat the object as a single row
* of data.
*
*
* @return the object for the value.
*/
public Object getValue() {
return getStateHelper().eval(PropertyKeys.value);
}
/**
*
* Set the value of the UIData
. This value must either be be of type {@link DataModel}, or a type that can
* be adapted into a {@link DataModel}.
*
*
* @param value the new value
*/
public void setValue(Object value) {
setDataModel(null);
getStateHelper().put(PropertyKeys.value, value);
}
// ----------------------------------------------------- UIComponent Methods
/**
*
* If "name" is something other than "value", "var", or "rowIndex", rely on the superclass conversion from
* ValueBinding
to ValueExpression
.
*
*
* @param name Name of the attribute or property for which to set a {@link ValueBinding}
* @param binding The {@link ValueBinding} to set, or null
to remove any currently set {@link ValueBinding}
*
* @throws IllegalArgumentException if name
is one of id
, parent
,
* var
, or rowIndex
* @throws NullPointerException if name
is null
* @deprecated This has been replaced by {@link #setValueExpression(java.lang.String, jakarta.el.ValueExpression)}.
*/
@Deprecated
@Override
public void setValueBinding(String name, ValueBinding binding) {
if (null != name) {
switch (name) {
case "value":
setDataModel(null);
break;
case "var":
case "rowIndex":
throw new IllegalArgumentException();
}
}
super.setValueBinding(name, binding);
}
/**
*
* Set the {@link ValueExpression} used to calculate the value for the specified attribute or property name, if any. In
* addition, if a {@link ValueExpression} is set for the value
property, remove any synthesized
* {@link DataModel} for the data previously bound to this component.
*
*
* @param name Name of the attribute or property for which to set a {@link ValueExpression}
* @param binding The {@link ValueExpression} to set, or null
to remove any currently set
* {@link ValueExpression}
*
* @throws IllegalArgumentException if name
is one of id
, parent
,
* var
, or rowIndex
* @throws NullPointerException if name
is null
* @since 1.2
*/
@Override
public void setValueExpression(String name, ValueExpression binding) {
if (null != name) {
switch (name) {
case "value":
model = null;
break;
case "var":
case "rowIndex":
throw new IllegalArgumentException();
}
}
super.setValueExpression(name, binding);
}
/**
*
* Return a client identifier for this component that includes the current value of the rowIndex
property,
* if it is not set to -1. This implies that multiple calls to getClientId()
may return different results,
* but ensures that child components can themselves generate row-specific client identifiers (since {@link UIData} is a
* {@link NamingContainer}).
*
*
* @throws NullPointerException if context
is null
*/
@Override
public String getClientId(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
// If baseClientId and clientIdBuilder are both null, this is the
// first time that getClientId() has been called.
// If we're not nested within another UIData, then:
// - create a new StringBuilder assigned to clientIdBuilder containing
// our client ID.
// - toString() the builder - this result will be our baseClientId
// for the duration of the component
// - append UINamingContainer.getSeparatorChar() to the builder
// If we are nested within another UIData, then:
// - create an empty StringBuilder that will be used to build
// this instance's ID
if (baseClientId == null && clientIdBuilder == null) {
if (!isNestedWithinIterator()) {
clientIdBuilder = new StringBuilder(super.getClientId(context));
baseClientId = clientIdBuilder.toString();
baseClientIdLength = baseClientId.length() + 1;
clientIdBuilder.append(UINamingContainer.getSeparatorChar(context));
clientIdBuilder.setLength(baseClientIdLength);
} else {
clientIdBuilder = new StringBuilder();
}
}
int rowIndex = getRowIndex();
if (rowIndex >= 0) {
String cid;
if (!isNestedWithinIterator()) {
// we're not nested, so the clientIdBuilder is already
// primed with clientID +
// UINamingContainer.getSeparatorChar(). Append the
// current rowIndex, and toString() the builder. reset
// the builder to it's primed state.
cid = clientIdBuilder.append(rowIndex).toString();
clientIdBuilder.setLength(baseClientIdLength);
} else {
// we're nested, so we have to build the ID from scratch
// each time. Reuse the same clientIdBuilder instance
// for each call by resetting the length to 0 after
// the ID has been computed.
cid = clientIdBuilder.append(super.getClientId(context)).append(UINamingContainer.getSeparatorChar(context)).append(rowIndex).toString();
clientIdBuilder.setLength(0);
}
return cid;
} else {
if (!isNestedWithinIterator()) {
// Not nested and no row available, so just return our baseClientId
return baseClientId;
} else {
// nested and no row available, return the result of getClientId().
// this is necessary as the client ID will reflect the row that
// this table represents
return super.getClientId(context);
}
}
}
/**
*
* Override behavior from {@link UIComponentBase#invokeOnComponent} to provide special care for positioning the data
* properly before finding the component and invoking the callback on it. If the argument clientId
is equal
* to this.getClientId()
simply invoke the contextCallback
, passing the context
* argument and this as arguments, and return true.
If the argument clientId
is not
* equal to this.getClientId()
, inspect each of the facet children of this UIData
instance and
* for each one, compare its clientId
with the argument clientId
. If there is a match, invoke
* the contextCallback
, passing the context
argument and this as arguments, and return
* true
. Otherwise, attempt to extract a rowIndex from the clientId
. For example, if the
* argument clientId
was form:data:3:customerHeader
the rowIndex would be 3
. Let
* this value be called newIndex
. The current rowIndex of this instance must be saved aside and restored
* before returning in all cases, regardless of the outcome of the search or if any exceptions are thrown in the
* process.
*
*
*
* The implementation of this method must never return true
if setting the rowIndex of this instance to be
* equal to newIndex
causes this instance to return false
from {@link #isRowAvailable}.
*
*
* @throws NullPointerException {@inheritDoc}
* @throws FacesException {@inheritDoc} Also throws FacesException
if any exception is thrown when deriving
* the rowIndex from the argument clientId
.
* @since 1.2
*/
@Override
public boolean invokeOnComponent(FacesContext context, String clientId, ContextCallback callback) throws FacesException {
if (null == context || null == clientId || null == callback) {
throw new NullPointerException();
}
String myId = super.getClientId(context);
boolean found = false;
if (clientId.equals(myId)) {
try {
pushComponentToEL(context, compositeParent);
callback.invokeContextCallback(context, this);
return true;
} catch (Exception e) {
throw new FacesException(e);
} finally {
popComponentFromEL(context);
}
}
// check the facets, if any, of UIData
if (getFacetCount() > 0) {
for (UIComponent c : getFacets().values()) {
if (clientId.equals(c.getClientId(context))) {
callback.invokeContextCallback(context, c);
return true;
}
}
}
// check column level facets, if any
if (getChildCount() > 0) {
for (UIComponent column : getChildren()) {
if (column instanceof UIColumn) {
if (column.getFacetCount() > 0) {
for (UIComponent facet : column.getFacets().values()) {
if (facet.invokeOnComponent(context, clientId, callback)) {
return true;
}
}
}
}
}
}
/*
* Check if we are looking for a component that is part of the actual skeleton.
*/
if (getChildCount() > 0) {
for (UIComponent column : getChildren()) {
if (column instanceof UIColumn) {
if (column.invokeOnComponent(context, clientId, callback)) {
return true;
}
}
}
}
int lastSep, newRow, savedRowIndex = getRowIndex();
char sepChar = UINamingContainer.getSeparatorChar(context);
// If we need to strip out the rowIndex from our id
// PENDING(edburns): is this safe with respect to I18N?
if (myId.endsWith(sepChar + Integer.toString(savedRowIndex, 10))) {
lastSep = myId.lastIndexOf(sepChar);
assert -1 != lastSep;
myId = myId.substring(0, lastSep);
}
// myId will be something like form:outerData for a non-nested table,
// and form:outerData:3:data for a nested table.
// clientId will be something like form:outerData:3:outerColumn
// for a non-nested table. clientId will be something like
// outerData:3:data:3:input for a nested table.
if (clientId.startsWith(myId)) {
try {
int preRowIndexSep, postRowIndexSep;
if (-1 != (preRowIndexSep = clientId.indexOf(sepChar, myId.length()))) {
// Check the length
if (++preRowIndexSep < clientId.length()) {
if (-1 != (postRowIndexSep = clientId.indexOf(sepChar, preRowIndexSep + 1))) {
try {
newRow = Integer.parseInt(clientId.substring(preRowIndexSep, postRowIndexSep));
} catch (NumberFormatException ex) {
// PENDING(edburns): I18N
String message = "Trying to extract rowIndex from clientId \'" + clientId + "\' " + ex.getMessage();
throw new NumberFormatException(message);
}
setRowIndex(newRow);
if (isRowAvailable()) {
found = super.invokeOnComponent(context, clientId, callback);
}
}
}
}
} catch (FacesException fe) {
throw fe;
} catch (NumberFormatException e) {
throw new FacesException(e);
} finally {
setRowIndex(savedRowIndex);
}
}
return found;
}
/**
*
* Override the default {@link UIComponentBase#queueEvent} processing to wrap any queued events in a wrapper so that we
* can reset the current row index in broadcast()
.
*
*
* @param event {@link FacesEvent} to be queued
*
* @throws IllegalStateException if this component is not a descendant of a {@link UIViewRoot}
* @throws NullPointerException if event
is null
*/
@Override
public void queueEvent(FacesEvent event) {
super.queueEvent(new WrapperEvent(this, event, getRowIndex()));
}
/**
*
* Override the default {@link UIComponentBase#broadcast} processing to unwrap any wrapped {@link FacesEvent} and reset
* the current row index, before the event is actually broadcast. For events that we did not wrap (in
* queueEvent()
), default processing will occur.
*
*
* @param event The {@link FacesEvent} to be broadcast
*
* @throws AbortProcessingException Signal the Jakarta Server Faces implementation that no further processing on the
* current event should be performed
* @throws IllegalArgumentException if the implementation class of this {@link FacesEvent} is not supported by this
* component
* @throws NullPointerException if event
is null
*/
@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
if (!(event instanceof WrapperEvent)) {
super.broadcast(event);
return;
}
FacesContext context = event.getFacesContext();
// Set up the correct context and fire our wrapped event
WrapperEvent revent = (WrapperEvent) event;
if (isNestedWithinIterator()) {
setDataModel(null);
}
int oldRowIndex = getRowIndex();
setRowIndex(revent.getRowIndex());
FacesEvent rowEvent = revent.getFacesEvent();
UIComponent source = rowEvent.getComponent();
UIComponent compositeParent = null;
try {
if (!UIComponent.isCompositeComponent(source)) {
compositeParent = UIComponent.getCompositeComponentParent(source);
}
if (compositeParent != null) {
compositeParent.pushComponentToEL(context, null);
}
source.pushComponentToEL(context, null);
source.broadcast(rowEvent);
} finally {
source.popComponentFromEL(context);
if (compositeParent != null) {
compositeParent.popComponentFromEL(context);
}
}
setRowIndex(oldRowIndex);
}
/**
*
* In addition to the default behavior, ensure that any saved per-row state for our child input components is discarded
* unless it is needed to rerender the current page with errors.
*
* @param context FacesContext for the current request
*
* @throws IOException if an input/output error occurs while rendering
* @throws NullPointerException if context
is null
*/
@Override
public void encodeBegin(FacesContext context) throws IOException {
preEncode(context);
super.encodeBegin(context);
}
/**
*
* Override the default {@link UIComponentBase#processDecodes} processing to perform the following steps.
*
*
* - If the
rendered
property of this {@link UIComponent} is false
, skip further
* processing.
* - Set the current
rowIndex
to -1.
* - Call the
processDecodes()
method of all facets of this {@link UIData}, in the order determined by a
* call to getFacets().keySet().iterator()
.
* - Call the
processDecodes()
method of all facets of the {@link UIColumn} children of this
* {@link UIData}.
* - Iterate over the set of rows that were included when this component was rendered (i.e. those defined by the
*
first
and rows
properties), performing the following processing for each row:
*
* - Set the current
rowIndex
to the appropriate value for this row.
* - If
isRowAvailable()
returns true
, iterate over the children components of each
* {@link UIColumn} child of this {@link UIData} component, calling the processDecodes()
method for each
* such child.
*
*
* - Set the current
rowIndex
to -1.
* - Call the
decode()
method of this component.
* - If a
RuntimeException
is thrown during decode processing, call {@link FacesContext#renderResponse}
* and re-throw the exception.
*
*
* @param context {@link FacesContext} for the current request
*
* @throws NullPointerException if context
is null
*/
@Override
public void processDecodes(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (!isRendered()) {
return;
}
pushComponentToEL(context, this);
preDecode(context);
iterate(context, PhaseId.APPLY_REQUEST_VALUES);
decode(context);
popComponentFromEL(context);
}
/**
*
* Override the default {@link UIComponentBase#processValidators} processing to perform the following steps.
*
*
* - If the
rendered
property of this {@link UIComponent} is false
, skip further
* processing.
* - Set the current
rowIndex
to -1.
* - Call the
processValidators()
method of all facets of this {@link UIData}, in the order determined by
* a call to getFacets().keySet().iterator()
.
* - Call the
processValidators()
method of all facets of the {@link UIColumn} children of this
* {@link UIData}.
* - Iterate over the set of rows that were included when this component was rendered (i.e. those defined by the
*
first
and rows
properties), performing the following processing for each row:
*
* - Set the current
rowIndex
to the appropriate value for this row.
* - If
isRowAvailable()
returns true
, iterate over the children components of each
* {@link UIColumn} child of this {@link UIData} component, calling the processValidators()
method for each
* such child.
*
*
* - Set the current
rowIndex
to -1.
*
*
* @param context {@link FacesContext} for the current request
* @throws NullPointerException if context
is null
* @see jakarta.faces.event.PreValidateEvent
* @see jakarta.faces.event.PostValidateEvent
*/
@Override
public void processValidators(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (!isRendered()) {
return;
}
pushComponentToEL(context, this);
Application app = context.getApplication();
app.publishEvent(context, PreValidateEvent.class, this);
preValidate(context);
iterate(context, PhaseId.PROCESS_VALIDATIONS);
app.publishEvent(context, PostValidateEvent.class, this);
popComponentFromEL(context);
}
/**
*
* Override the default {@link UIComponentBase#processUpdates} processing to perform the following steps.
*
*
* - If the
rendered
property of this {@link UIComponent} is false
, skip further
* processing.
* - Set the current
rowIndex
to -1.
* - Call the
processUpdates()
method of all facets of this {@link UIData}, in the order determined by a
* call to getFacets().keySet().iterator()
.
* - Call the
processUpdates()
method of all facets of the {@link UIColumn} children of this
* {@link UIData}.
* - Iterate over the set of rows that were included when this component was rendered (i.e. those defined by the
*
first
and rows
properties), performing the following processing for each row:
*
* - Set the current
rowIndex
to the appropriate value for this row.
* - If
isRowAvailable()
returns true
, iterate over the children components of each
* {@link UIColumn} child of this {@link UIData} component, calling the processUpdates()
method for each
* such child.
*
*
* - Set the current
rowIndex
to -1.
*
*
* @param context {@link FacesContext} for the current request
*
* @throws NullPointerException if context
is null
*/
@Override
public void processUpdates(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (!isRendered()) {
return;
}
pushComponentToEL(context, this);
preUpdate(context);
iterate(context, PhaseId.UPDATE_MODEL_VALUES);
popComponentFromEL(context);
// This is not a EditableValueHolder, so no further processing is required
}
@Override
public String createUniqueId(FacesContext context, String seed) {
Integer i = (Integer) getStateHelper().get(PropertyKeys.lastId);
int lastId = i != null ? i : 0;
getStateHelper().put(PropertyKeys.lastId, ++lastId);
return UIViewRoot.UNIQUE_ID_PREFIX + (seed == null ? lastId : seed);
}
/**
*
* Override the behavior in {@link UIComponent#visitTree} to handle
* iteration correctly.
*
*
*
*
*
* If the {@link UIComponent#isVisitable} method of this instance returns false
, take no action and return.
*
*
*
* Call {@link UIComponent#pushComponentToEL} and invoke the visit callback on this UIData
instance as
* described in {@link UIComponent#visitTree}. Let the result of the invoctaion be visitResult. If
* visitResult is {@link VisitResult#COMPLETE}, take no further action and return true
. Otherwise,
* determine if we need to visit our children. The default implementation calls
* {@link VisitContext#getSubtreeIdsToVisit} passing this
as the argument. If the result of that call is
* non-empty, let doVisitChildren be true
. If doVisitChildren is true
and
* visitResult is {@link VisitResult#ACCEPT}, take the following action.
*
*
*
*
* -
*
* If this component has facets, call {@link UIComponent#getFacets} on this instance and invoke the
* values()
method. For each UIComponent
in the returned Map
, call
* {@link UIComponent#visitTree}.
*
*
*
* -
*
*
*
*
* If this component has children, for each UIColumn
child:
*
*
*
* Call {@link VisitContext#invokeVisitCallback} on that UIColumn
instance. If such a call returns
* true
, terminate visiting and return true
from this method.
*
*
*
* If the child UIColumn
has facets, call {@link UIComponent#visitTree} on each one.
*
*
*
* Take no action on non-UIColumn
children.
*
*
*
*
* -
*
*
*
*
* Save aside the result of a call to {@link #getRowIndex}.
*
*
*
* For each child component of this UIData
that is also an instance of {@link UIColumn},
*
*
*
* Iterate over the rows.
*
*
*
*
*
*
* -
*
* Let rowsToProcess be the return from {@link #getRows}.
*
*
*
* -
*
* Let rowIndex be the return from {@link #getFirst} - 1.
*
*
*
* -
*
* While the number of rows processed is less than rowsToProcess, take the following actions.
*
*
*
* Call {@link #setRowIndex}, passing the current row index.
*
*
*
* If {@link #isRowAvailable} returns false
, take no further action and return false
.
*
*
*
* Call {@link UIComponent#visitTree} on each of the children of this UIColumn
instance.
*
*
*
*
*
*
*
*
*
*
*
* Call {@link #popComponentFromEL} and restore the saved row index with a call to {@link #setRowIndex}.
*
*
*
* Return false
to allow the visiting to continue.
*
*
*
*
* @param context the VisitContext
that provides context for performing the visit.
*
* @param callback the callback to be invoked for each node encountered in the visit.
*
* @throws NullPointerException if any of the parameters are null
.
*
*
*/
@Override
public boolean visitTree(VisitContext context, VisitCallback callback) {
// First check to see whether we are visitable. If not
// short-circuit out of this subtree, though allow the
// visit to proceed through to other subtrees.
if (!isVisitable(context)) {
return false;
}
FacesContext facesContext = context.getFacesContext();
// NOTE: that the visitRows local will be obsolete once the
// appropriate visit hints have been added to the API
boolean visitRows = requiresRowIteration(context);
// Clear out the row index is one is set so that
// we start from a clean slate.
int oldRowIndex = -1;
if (visitRows) {
oldRowIndex = getRowIndex();
setRowIndex(-1);
}
// Push ourselves to EL
pushComponentToEL(facesContext, null);
try {
// Visit ourselves. Note that we delegate to the
// VisitContext to actually perform the visit.
VisitResult result = context.invokeVisitCallback(this, callback);
// If the visit is complete, short-circuit out and end the visit
if (result == VisitResult.COMPLETE) {
return true;
}
// Visit children, short-circuiting as necessary
// NOTE: that the visitRows parameter will be obsolete once the
// appropriate visit hints have been added to the API
if (result == VisitResult.ACCEPT && doVisitChildren(context, visitRows)) {
// First visit facets
// NOTE: that the visitRows parameter will be obsolete once the
// appropriate visit hints have been added to the API
if (visitFacets(context, callback, visitRows)) {
return true;
}
// Next column facets
// NOTE: that the visitRows parameter will be obsolete once the
// appropriate visit hints have been added to the API
if (visitColumnsAndColumnFacets(context, callback, visitRows)) {
return true;
}
// And finally, visit rows
// NOTE: that the visitRows parameter will be obsolete once the
// appropriate visit hints have been added to the API
if (visitRows(context, callback, visitRows)) {
return true;
}
}
} finally {
// Clean up - pop Jakarta Expression Language and restore old row index
popComponentFromEL(facesContext);
if (visitRows) {
setRowIndex(oldRowIndex);
}
}
// Return false to allow the visit to continue
return false;
}
/**
*
* Override the base class method to take special action if the method is being invoked when
* {@link StateManager#IS_BUILDING_INITIAL_STATE} is true and the rowStatePreserved
* JavaBeans property for this instance is true
.
*
*
*
* The additional action taken is to traverse the descendents and save their state without regard to any particular row
* value.
*
*
* @since 2.1
*/
@Override
public void markInitialState() {
if (isRowStatePreserved()) {
if (getFacesContext().getAttributes().containsKey(StateManager.IS_BUILDING_INITIAL_STATE)) {
_initialDescendantFullComponentState = saveDescendantInitialComponentStates(getFacesContext(), getChildren().iterator(), false);
}
}
super.markInitialState();
}
private void restoreFullDescendantComponentStates(FacesContext facesContext, Iterator childIterator, Object state,
boolean restoreChildFacets) {
Iterator extends Object[]> descendantStateIterator = null;
while (childIterator.hasNext()) {
if (descendantStateIterator == null && state != null) {
descendantStateIterator = ((Collection extends Object[]>) state).iterator();
}
UIComponent component = childIterator.next();
// reset the client id (see spec 3.1.6)
component.setId(component.getId());
if (!component.isTransient()) {
Object childState = null;
Object descendantState = null;
if (descendantStateIterator != null && descendantStateIterator.hasNext()) {
Object[] object = descendantStateIterator.next();
childState = object[0];
descendantState = object[1];
}
component.clearInitialState();
component.restoreState(facesContext, childState);
component.markInitialState();
Iterator childsIterator;
if (restoreChildFacets) {
childsIterator = component.getFacetsAndChildren();
} else {
childsIterator = component.getChildren().iterator();
}
restoreFullDescendantComponentStates(facesContext, childsIterator, descendantState, true);
}
}
}
private Collection