You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by bo...@apache.org on 2024/12/02 18:42:31 UTC

(myfaces-tobago) branch main updated: feat(sheet): readonlyRows attribute (#5722)

This is an automated email from the ASF dual-hosted git repository.

bommel pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git


The following commit(s) were added to refs/heads/main by this push:
     new 1c4bbca32d feat(sheet): readonlyRows attribute (#5722)
1c4bbca32d is described below

commit 1c4bbca32d91bc6adb11beb494ac9e21947c61e7
Author: Bernd Bohmann <bo...@apache.org>
AuthorDate: Mon Dec 2 19:42:25 2024 +0100

    feat(sheet): readonlyRows attribute (#5722)
    
    Performance optimization hint used during
    ApplyRequestValues, Validation and UpdateModel
    on columnChildren.
    
    issue: TOBAGO-2370
---
 .../myfaces/tobago/component/Attributes.java       |   1 +
 .../tobago/internal/component/AbstractUIData.java  |  54 -----
 .../tobago/internal/component/AbstractUISheet.java | 256 ++++++++++++++++++++-
 .../taglib/component/SheetTagDeclaration.java      |  17 ++
 4 files changed, 273 insertions(+), 55 deletions(-)

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 e4f2ec8338..8e7ae19964 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 jakarta.faces.FacesException;
 import jakarta.faces.component.ContextCallback;
 import jakarta.faces.component.UIComponent;
 import jakarta.faces.component.UINamingContainer;
-import jakarta.faces.component.visit.VisitCallback;
-import jakarta.faces.component.visit.VisitContext;
 import jakarta.faces.context.FacesContext;
 import jakarta.faces.model.DataModel;
 
@@ -290,56 +288,4 @@ public abstract class AbstractUIData extends jakarta.faces.component.UIData impl
       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 6036c9115a..15893d147c 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
@@ -19,6 +19,12 @@
 
 package org.apache.myfaces.tobago.internal.component;
 
+import jakarta.faces.component.UINamingContainer;
+import jakarta.faces.component.behavior.ClientBehaviorContext;
+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 org.apache.myfaces.tobago.component.Attributes;
 import org.apache.myfaces.tobago.component.Pageable;
 import org.apache.myfaces.tobago.component.Visual;
@@ -62,8 +68,12 @@ import jakarta.faces.event.PreRenderComponentEvent;
 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 abstract class AbstractUISheet extends AbstractUIData
     }
   }
 
+  @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 abstract class AbstractUISheet extends AbstractUIData
     }
   }
 
+  private void process(FacesContext context, boolean skipColumnChildren,
+                       BiConsumer<FacesContext, UIComponent> 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<FacesContext, UIComponent> 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<FacesContext, UIComponent> 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 abstract class AbstractUISheet extends AbstractUIData
     }
   }
 
+  @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<String> 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<Integer> 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<Integer> getRowsToVisit(final VisitContext context) {
+    Set<Integer> rowsToVisit = new HashSet<>();
+    FacesContext facesContext = context.getFacesContext();
+    String clientId = getClientId(facesContext);
+    int clientIdLengthPlusOne = clientId.length() + 1;
+    char separatorChar = UINamingContainer.getSeparatorChar(facesContext);
+    Collection<String> 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 abstract class AbstractUISheet extends AbstractUIData
   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 9af7a4d4ac..0e8529bc1a 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 jakarta.faces.event.PhaseId#APPLY_REQUEST_VALUES} and
+   * {@link jakarta.faces.event.PhaseId#PROCESS_VALIDATIONS} and
+   * {@link jakarta.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 jakarta.faces.component.EditableValueHolder} components in the sheet rows.
+   *
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(type = "boolean", defaultValue = "false")
+  void setReadonlyRows(String readonly);
+
 }