diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java index 03b89f3a8c..f95f7248b2 100644 --- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java +++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java @@ -341,6 +341,7 @@ public enum Attributes { preferredWidth, preformated, readonly, + readonlyRows, reference, rel, relative, diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIData.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIData.java index 8295f90d10..fd58095493 100644 --- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIData.java +++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIData.java @@ -33,8 +33,6 @@ import javax.faces.component.ContextCallback; import javax.faces.component.UIComponent; import javax.faces.component.UINamingContainer; -import javax.faces.component.visit.VisitCallback; -import javax.faces.component.visit.VisitContext; import javax.faces.context.FacesContext; import javax.faces.model.DataModel; import javax.swing.tree.TreeNode; @@ -289,56 +287,4 @@ public List getRowIndicesOfChildren() { return null; } } - - /** - * This is, because we need to visit the UIRow for each row, which is not done in the base implementation. - */ - @Override - public boolean visitTree(final VisitContext context, final VisitCallback callback) { - - if (super.visitTree(context, callback)) { - return true; - } - - // save the current row index - final int oldRowIndex = getRowIndex(); - // set row index to -1 to process the facets and to get the rowless clientId - setRowIndex(-1); - // push the Component to EL - pushComponentToEL(context.getFacesContext(), this); - - try { - // iterate over the rows - int rowsToProcess = getRows(); - // if getRows() returns 0, all rows have to be processed - if (rowsToProcess == 0) { - rowsToProcess = getRowCount(); - } - int rowIndex = getFirst(); - for (int rowsProcessed = 0; rowsProcessed < rowsToProcess; rowsProcessed++, rowIndex++) { - setRowIndex(rowIndex); - if (!isRowAvailable()) { - return false; - } - // visit the children of every child of the UIData that is an instance of UIColumn - for (int i = 0, childCount = getChildCount(); i < childCount; i++) { - final UIComponent child = getChildren().get(i); - if (child instanceof AbstractUIRow) { - if (child.visitTree(context, callback)) { - return true; - } - - } - } - } - } finally { - // pop the component from EL and restore the old row index - popComponentFromEL(context.getFacesContext()); - setRowIndex(oldRowIndex); - } - - // Return false to allow the visiting to continue - return false; - } - } diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java index 9b47674848..8ec9795bba 100644 --- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java +++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java @@ -48,9 +48,15 @@ import javax.el.ValueExpression; import javax.faces.component.UIColumn; import javax.faces.component.UIComponent; +import javax.faces.component.UINamingContainer; import javax.faces.component.behavior.AjaxBehavior; import javax.faces.component.behavior.ClientBehavior; +import javax.faces.component.behavior.ClientBehaviorContext; import javax.faces.component.behavior.ClientBehaviorHolder; +import javax.faces.component.visit.VisitCallback; +import javax.faces.component.visit.VisitContext; +import javax.faces.component.visit.VisitHint; +import javax.faces.component.visit.VisitResult; import javax.faces.context.FacesContext; import javax.faces.event.AbortProcessingException; import javax.faces.event.ComponentSystemEvent; @@ -62,8 +68,12 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; /** * {@link org.apache.myfaces.tobago.internal.taglib.component.SheetTagDeclaration} @@ -326,9 +336,19 @@ public int getFirstRowIndexOfLastPage() { } } + @Override + public void processDecodes(FacesContext context) { + process(context, isReadonlyRows(), (fc, uic) -> uic.processDecodes(fc)); + } + + @Override + public void processValidators(FacesContext context) { + process(context, isReadonlyRows(), (fc, uic) -> uic.processValidators(fc)); + } + @Override public void processUpdates(final FacesContext context) { - super.processUpdates(context); + process(context, isReadonlyRows(), (fc, uic) -> uic.processUpdates(fc)); final SheetState sheetState = getSheetState(context); if (sheetState != null) { @@ -339,6 +359,112 @@ public void processUpdates(final FacesContext context) { } } + private void process(FacesContext context, boolean skipColumnChildren, + BiConsumer consumer) { + try { + pushComponentToEL(context, this); + if (!isRendered()) { + return; + } + setRowIndex(-1); + if (this.getFacetCount() > 0) { + for (UIComponent facet : getFacets().values()) { + consumer.accept(context, facet); + } + } + boolean[] columnRendered = new boolean[getChildCount()]; + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + UIComponent child = getChildren().get(i); + if (child instanceof UIColumn) { + try { + child.pushComponentToEL(context, child); + if (child.isRendered()) { + columnRendered[i] = true; + } else { + continue; + } + } finally { + child.popComponentFromEL(context); + } + if (child.getFacetCount() > 0) { + for (UIComponent facet : child.getFacets().values()) { + consumer.accept(context, facet); + } + } + } + } + if (skipColumnChildren) { + // process action source + int rowIndex = getRowFromActionSource(context); + processRow(context, columnRendered, consumer, rowIndex); + } else { + processColumnChildren(context, columnRendered, consumer); + } + setRowIndex(-1); + try { + decode(context); + } catch (RuntimeException e) { + context.renderResponse(); + throw e; + } + } finally { + popComponentFromEL(context); + } + } + + private void processColumnChildren(FacesContext context, boolean[] childRendered, + BiConsumer consumer) { + int first = getFirst(); + int rows = getRows(); + int last; + if (rows == 0) { + last = getRowCount(); + } else { + last = first + rows; + } + for (int rowIndex = first; last == -1 || rowIndex < last; rowIndex++) { + if (processRow(context, childRendered, consumer, rowIndex)) { + break; + } + } + } + + private boolean processRow(FacesContext context, boolean[] childRendered, + BiConsumer consumer, int rowIndex) { + setRowIndex(rowIndex); + + // scrolled past the last row + if (!isRowAvailable()) { + return true; + } + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + if (childRendered[i]) { + UIComponent child = getChildren().get(i); + if (child instanceof AbstractUIRow) { + consumer.accept(context, child); + } else { + for (int j = 0, columnChildCount = child.getChildCount(); j < columnChildCount; j++) { + UIComponent columnChild = child.getChildren().get(j); + consumer.accept(context, columnChild); + } + } + } + } + return false; + } + + private int getRowFromActionSource(FacesContext facesContext) { + String clientId = getClientId(facesContext); + int clientIdLengthPlusOne = clientId.length() + 1; + char separatorChar = UINamingContainer.getSeparatorChar(facesContext); + final String sourceId = facesContext + .getExternalContext().getRequestParameterMap().get(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME); + if (sourceId != null && sourceId.startsWith(clientId)) { + return getRowIndexFromSubtreeId(sourceId, separatorChar, clientIdLengthPlusOne); + } + return -1; + } + @Override public Object saveState(final FacesContext context) { final Object[] saveState = new Object[2]; @@ -411,6 +537,132 @@ public void broadcast(final FacesEvent facesEvent) throws AbortProcessingExcepti } } + @Override + public boolean visitTree(final VisitContext context, final VisitCallback callback) { + boolean skipIterationHint = context.getHints().contains(VisitHint.SKIP_ITERATION); + if (skipIterationHint) { + return super.visitTree(context, callback); + } + FacesContext facesContext = context.getFacesContext(); + pushComponentToEL(facesContext, this); + if (!isVisitable(context)) { + return false; + } + + // save the current row index + int oldRowIndex = getRowIndex(); + try { + // set row index to -1 to process the facets and to get the rowless clientId + setRowIndex(-1); + VisitResult visitResult = context.invokeVisitCallback(this, callback); + switch (visitResult) { + case COMPLETE: + //we are done nothing has to be processed anymore + return true; + case REJECT: + return false; + default: + // accept; determine if we need to visit our children + Collection subtreeIdsToVisit = context.getSubtreeIdsToVisit(this); + boolean doVisitChildren = subtreeIdsToVisit != null && !subtreeIdsToVisit.isEmpty(); + if (doVisitChildren) { + // visit the facets of the component + if (getFacetCount() > 0) { + for (UIComponent facet : getFacets().values()) { + if (facet.visitTree(context, callback)) { + return true; + } + } + } + // visit every column directly without visiting its children + // (the children of every UIColumn will be visited later for + // every row) and also visit the column's facets + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + UIComponent child = getChildren().get(i); + if (child instanceof UIColumn) { + VisitResult columnResult = context.invokeVisitCallback(child, callback); + if (columnResult == VisitResult.COMPLETE) { + return true; + } + if (child.getFacetCount() > 0) { + for (UIComponent facet : child.getFacets().values()) { + if (facet.visitTree(context, callback)) { + return true; + } + } + } + } + } + Set rowsToVisit = getRowsToVisit(context); + if (rowsToVisit.isEmpty()) { + return false; + } + // iterate over the rows to visit + for (Integer rowIndex : rowsToVisit) { + setRowIndex(rowIndex); + if (!isRowAvailable()) { + return false; + } + // visit the children of every child of the UIData that is an instance of UIColumn + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final UIComponent child = getChildren().get(i); + if (child instanceof UIColumn) { + if (child instanceof AbstractUIRow) { + if (child.visitTree(context, callback)) { + return true; + } + } else { + for (int j = 0, grandChildCount = child.getChildCount(); + j < grandChildCount; j++) { + UIComponent grandchild = child.getChildren().get(j); + if (grandchild.visitTree(context, callback)) { + return true; + } + } + } + } + } + } + } + } + } finally { + // pop the component from EL and restore the old row index + popComponentFromEL(facesContext); + setRowIndex(oldRowIndex); + } + // Return false to allow the visiting to continue + return false; + } + + private Set getRowsToVisit(final VisitContext context) { + Set rowsToVisit = new HashSet<>(); + FacesContext facesContext = context.getFacesContext(); + String clientId = getClientId(facesContext); + int clientIdLengthPlusOne = clientId.length() + 1; + char separatorChar = UINamingContainer.getSeparatorChar(facesContext); + Collection subtreeIdsToVisit = context.getSubtreeIdsToVisit(this); + for (String subtreeId : subtreeIdsToVisit) { + int rowIndex = getRowIndexFromSubtreeId(subtreeId, separatorChar, clientIdLengthPlusOne); + if (rowIndex != -1) { + rowsToVisit.add(rowIndex); + } + } + return rowsToVisit; + } + + private int getRowIndexFromSubtreeId(String sourceId, char separatorChar, int clientIdLengthPlusOne) { + int index = sourceId.indexOf(separatorChar, clientIdLengthPlusOne); + if (index != -1) { + String possibleRowIndex = sourceId.substring(clientIdLengthPlusOne, index); + try { + return Integer.parseInt(possibleRowIndex); + } catch (final NumberFormatException e) { + // ignore + } + } + return -1; + } + public void init(final FacesContext facesContext) { sort(facesContext, null); layoutHeader(); @@ -650,4 +902,6 @@ public void setHeaderGrid(final Grid headerGrid) { public abstract Integer getLazyRows(); public abstract PaginatorMode getPaginator(); + + public abstract boolean isReadonlyRows(); } diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java index d93eed2243..49947acaa6 100644 --- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java +++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java @@ -380,4 +380,21 @@ public interface SheetTagDeclaration }, defaultCode = "org.apache.myfaces.tobago.layout.PaginatorMode.useShowAttributes") void setPaginator(String paginator); + + /** + * Flag indicating that the rows of the sheet are readonly. + * The readonly attribute is a performance optimization hint used during + * {@link javax.faces.event.PhaseId#APPLY_REQUEST_VALUES} and + * {@link javax.faces.event.PhaseId#PROCESS_VALIDATIONS} and + * {@link javax.faces.event.PhaseId#UPDATE_MODEL_VALUES}. + * When set to true, it signals the rows of the sheet are read-only und + * doesn't require updates potentially saving processing time. + * This optimization should only be applied when there are no non-readonly + * {@link javax.faces.component.EditableValueHolder} components in the sheet rows. + * + */ + @TagAttribute + @UIComponentTagAttribute(type = "boolean", defaultValue = "false") + void setReadonlyRows(String readonly); + }