Repository: primefaces/primefaces (java)

Issue:
DataTable/TreeTable: support composite as filter
https://github.com/orgs/primefaces/discussions/691#discussioncomment-7868229

and

related to #11092

Pull request description:
Fixes #11135 - DataTable/TreeTable: support composite as filter
Fixes #11135

Patch:
diff --git a/primefaces/src/main/java/org/primefaces/component/api/UIColumn.java b/primefaces/src/main/java/org/primefaces/component/api/UIColumn.java
index 35256ded6f..3019d8f582 100644
--- a/primefaces/src/main/java/org/primefaces/component/api/UIColumn.java
+++ b/primefaces/src/main/java/org/primefaces/component/api/UIColumn.java
@@ -23,10 +23,11 @@
  */
 package org.primefaces.component.api;
 
-import java.io.IOException;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.primefaces.component.celleditor.CellEditor;
+import org.primefaces.model.MatchMode;
+import org.primefaces.util.FacetUtils;
+import org.primefaces.util.LangUtils;
+
 import javax.el.ELContext;
 import javax.el.MethodExpression;
 import javax.el.ValueExpression;
@@ -35,11 +36,11 @@ import javax.faces.component.EditableValueHolder;
 import javax.faces.component.UIComponent;
 import javax.faces.component.UIData;
 import javax.faces.context.FacesContext;
-
-import org.primefaces.component.celleditor.CellEditor;
-import org.primefaces.model.MatchMode;
-import org.primefaces.util.ComponentTraversalUtils;
-import org.primefaces.util.LangUtils;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public interface UIColumn {
 
@@ -246,9 +247,26 @@ public interface UIColumn {
 
     Object getConverter();
 
-    default EditableValueHolder getFilterValueHolder() {
+    default Object getFilterValueFromValueHolder() {
         UIComponent filterFacet = getFacet("filter");
-        return ComponentTraversalUtils.firstChildRenderedOrSelf(EditableValueHolder.class, filterFacet);
+        if (filterFacet == null) {
+            return null;
+        }
+        AtomicReference<Object> filterValue = new AtomicReference<>(null);
+
+        FacetUtils.invokeOnEditableValueHolder(FacesContext.getCurrentInstance(), filterFacet, (ctx, component) -> {
+            filterValue.set(((EditableValueHolder) component).getValue());
+        });
+
+        return filterValue.get();
+    }
+
+    default void setFilterValueToValueHolder(Object value) {
+        UIComponent filterFacet = getFacet("filter");
+
+        FacetUtils.invokeOnEditableValueHolder(FacesContext.getCurrentInstance(), filterFacet, (ctx, component) -> {
+            ((EditableValueHolder) component).setValue(value);
+        });
     }
 
     default UIComponent asUIComponent() {
diff --git a/primefaces/src/main/java/org/primefaces/component/api/UITable.java b/primefaces/src/main/java/org/primefaces/component/api/UITable.java
index 7365ca8b10..4b0e7bc8cd 100644
--- a/primefaces/src/main/java/org/primefaces/component/api/UITable.java
+++ b/primefaces/src/main/java/org/primefaces/component/api/UITable.java
@@ -23,20 +23,6 @@
  */
 package org.primefaces.component.api;
 
-import java.util.*;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import javax.el.MethodExpression;
-import javax.el.ValueExpression;
-import javax.faces.FacesException;
-import javax.faces.component.UIComponent;
-import javax.faces.component.UINamingContainer;
-import javax.faces.component.ValueHolder;
-import javax.faces.component.search.SearchExpressionHint;
-import javax.faces.context.FacesContext;
-import javax.faces.convert.ConverterException;
-
 import org.primefaces.component.column.ColumnBase;
 import org.primefaces.component.headerrow.HeaderRow;
 import org.primefaces.expression.SearchExpressionUtils;
@@ -48,6 +34,20 @@ import org.primefaces.util.FacetUtils;
 import org.primefaces.util.LangUtils;
 import org.primefaces.util.LocaleUtils;
 
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UINamingContainer;
+import javax.faces.component.ValueHolder;
+import javax.faces.component.search.SearchExpressionHint;
+import javax.faces.context.FacesContext;
+import javax.faces.convert.ConverterException;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
 public interface UITable<T extends UITableState> extends ColumnAware, MultiViewStateAware<T> {
 
     /**
@@ -177,8 +177,12 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
 
         FilterMeta globalFilter = filterBy.get(FilterMeta.GLOBAL_FILTER_KEY);
         if (globalFilter != null) {
-            globalFilter.setFilterValue(
-                    params.get(((UIComponent) this).getClientId(context) + separator + FilterMeta.GLOBAL_FILTER_KEY));
+            String componentIdPrefix = ((UIComponent) this).getClientId(context) + separator + FilterMeta.GLOBAL_FILTER_KEY;
+            String filterValue = params.entrySet().stream()
+                    .filter(e -> e.getKey() != null && e.getKey().startsWith(componentIdPrefix))
+                    .map(e -> e.getValue())
+                    .findFirst().orElse(null);
+            globalFilter.setFilterValue(filterValue);
         }
 
         forEachColumn(column -> {
@@ -191,12 +195,11 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
                 ((DynamicColumn) column).applyModel();
             }
 
-            ValueHolder filterValueHolder = column.getFilterValueHolder();
-            boolean hasCustomFilter = filterValueHolder != null;
+            boolean hasCustomFilter = column.getFacet("filter") != null;
 
             Object filterValue;
             if (hasCustomFilter) {
-                filterValue = filterValueHolder.getLocalValue();
+                filterValue = column.getFilterValueFromValueHolder();
             }
             else {
                 String valueHolderClientId = column instanceof DynamicColumn
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
index 84b79d5a4e..74bd44e276 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
@@ -23,24 +23,6 @@
  */
 package org.primefaces.component.datatable;
 
-import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import javax.el.ELContext;
-import javax.el.MethodExpression;
-import javax.el.ValueExpression;
-import javax.faces.FacesException;
-import javax.faces.component.UIComponent;
-import javax.faces.component.UINamingContainer;
-import javax.faces.component.ValueHolder;
-import javax.faces.context.FacesContext;
-import javax.faces.context.ResponseWriter;
-
 import org.primefaces.component.api.DynamicColumn;
 import org.primefaces.component.api.UIColumn;
 import org.primefaces.component.celleditor.CellEditor;
@@ -58,7 +40,30 @@ import org.primefaces.model.ColumnMeta;
 import org.primefaces.model.SortMeta;
 import org.primefaces.model.SortOrder;
 import org.primefaces.renderkit.DataRenderer;
-import org.primefaces.util.*;
+import org.primefaces.util.ComponentUtils;
+import org.primefaces.util.Constants;
+import org.primefaces.util.FacetUtils;
+import org.primefaces.util.HTML;
+import org.primefaces.util.LangUtils;
+import org.primefaces.util.MessageFactory;
+import org.primefaces.util.WidgetBuilder;
+
+import javax.el.ELContext;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UINamingContainer;
+import javax.faces.context.FacesContext;
+import javax.faces.context.ResponseWriter;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 public class DataTableRenderer extends DataRenderer {
 
@@ -752,8 +757,7 @@ public class DataTableRenderer extends DataRenderer {
         }
         else {
             Object filterValue = table.getFilterValue(column);
-            ValueHolder filterValueHolder = column.getFilterValueHolder();
-            filterValueHolder.setValue(filterValue);
+            column.setFilterValueToValueHolder(filterValue);
 
             writer.startElement("div", null);
             writer.writeAttribute("class", DataTable.COLUMN_CUSTOM_FILTER_CLASS, null);
diff --git a/primefaces/src/main/java/org/primefaces/event/CellEditEvent.java b/primefaces/src/main/java/org/primefaces/event/CellEditEvent.java
index c925ac97fc..f52f0513ca 100644
--- a/primefaces/src/main/java/org/primefaces/event/CellEditEvent.java
+++ b/primefaces/src/main/java/org/primefaces/event/CellEditEvent.java
@@ -33,8 +33,8 @@ import org.primefaces.component.treetable.TreeTable;
 import org.primefaces.util.FacetUtils;
 
 import javax.faces.FacesException;
+import javax.faces.component.EditableValueHolder;
 import javax.faces.component.UIComponent;
-import javax.faces.component.ValueHolder;
 import javax.faces.component.behavior.Behavior;
 import javax.faces.context.FacesContext;
 import java.util.ArrayList;
@@ -121,8 +121,8 @@ public class CellEditEvent<T> extends AbstractAjaxBehaviorEvent {
                 AtomicBoolean invoked = new AtomicBoolean(false);
                 List<Object> values = new ArrayList<>(1);
 
-                FacetUtils.invokeOnValueHolder(FacesContext.getCurrentInstance(), inputFacet, (ctx, component) -> {
-                    values.add(((ValueHolder) component).getValue());
+                FacetUtils.invokeOnEditableValueHolder(FacesContext.getCurrentInstance(), inputFacet, (ctx, component) -> {
+                    values.add(((EditableValueHolder) component).getValue());
                     invoked.set(true);
                 });
 
diff --git a/primefaces/src/main/java/org/primefaces/model/FilterMeta.java b/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
index 9e735f3b05..7226d7c156 100644
--- a/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
+++ b/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
@@ -23,16 +23,6 @@
  */
 package org.primefaces.model;
 
-import java.io.Serializable;
-import java.lang.reflect.Array;
-import java.util.Collection;
-import java.util.Objects;
-import javax.el.ELContext;
-import javax.el.MethodExpression;
-import javax.el.ValueExpression;
-import javax.faces.component.ValueHolder;
-import javax.faces.context.FacesContext;
-
 import org.primefaces.component.api.DynamicColumn;
 import org.primefaces.component.api.UIColumn;
 import org.primefaces.component.column.ColumnBase;
@@ -42,6 +32,15 @@ import org.primefaces.model.filter.FunctionFilterConstraint;
 import org.primefaces.model.filter.GlobalFilterConstraint;
 import org.primefaces.util.LangUtils;
 
+import javax.el.ELContext;
+import javax.el.MethodExpression;
+import javax.el.ValueExpression;
+import javax.faces.context.FacesContext;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Objects;
+
 public class FilterMeta implements Serializable {
 
     public static final String GLOBAL_FILTER_KEY = "globalFilter";
@@ -100,10 +99,7 @@ public class FilterMeta implements Serializable {
 
         Object filterValue = column.getFilterValue();
         if (filterValue == null) {
-            ValueHolder valueHolder = column.getFilterValueHolder();
-            if (valueHolder != null) {
-                filterValue = valueHolder.getValue();
-            }
+            filterValue = column.getFilterValueFromValueHolder();
         }
 
         return new FilterMeta(column.getColumnKey(),
diff --git a/primefaces/src/main/java/org/primefaces/util/ComponentTraversalUtils.java b/primefaces/src/main/java/org/primefaces/util/ComponentTraversalUtils.java
index 03b5fb2c76..36f48a102a 100644
--- a/primefaces/src/main/java/org/primefaces/util/ComponentTraversalUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/ComponentTraversalUtils.java
@@ -23,11 +23,15 @@
  */
 package org.primefaces.util;
 
+import javax.faces.component.ContextCallback;
+import javax.faces.component.NamingContainer;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIForm;
+import javax.faces.component.UniqueIdVendor;
+import javax.faces.context.FacesContext;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
-import javax.faces.component.*;
-import javax.faces.context.FacesContext;
 
 public class ComponentTraversalUtils {
 
@@ -190,17 +194,6 @@ public class ComponentTraversalUtils {
         return (UIComponent) closest(NamingContainer.class, component);
     }
 
-    public static <T> T firstChildRenderedOrSelf(Class<T> childType, UIComponent base) {
-        if (base == null || !base.isRendered()) {
-            return null;
-        }
-        else if (childType.isInstance(base)) {
-            return (T) base;
-        }
-
-        return firstChildRendered(childType, base);
-    }
-
     public static <T> T firstChildRendered(Class<T> childType, UIComponent base) {
         if (base == null || !base.isRendered()) {
             return null;
diff --git a/primefaces/src/main/java/org/primefaces/util/FacetUtils.java b/primefaces/src/main/java/org/primefaces/util/FacetUtils.java
index c8a612232a..08e134b30e 100644
--- a/primefaces/src/main/java/org/primefaces/util/FacetUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/FacetUtils.java
@@ -24,9 +24,11 @@
 package org.primefaces.util;
 
 import javax.faces.component.ContextCallback;
+import javax.faces.component.EditableValueHolder;
 import javax.faces.component.UIComponent;
 import javax.faces.component.UIPanel;
-import javax.faces.component.ValueHolder;
+import javax.faces.component.visit.VisitContext;
+import javax.faces.component.visit.VisitResult;
 import javax.faces.context.FacesContext;
 
 public class FacetUtils {
@@ -78,27 +80,25 @@ public class FacetUtils {
         return shouldRenderFacet(facet, false);
     }
 
-    public static void invokeOnValueHolder(FacesContext context, UIComponent facet, ContextCallback callback) {
-        // the facet contains multiple childs, so its wrapped inside a UIPanel
-        if (facet instanceof UIPanel) {
-            for (int i = 0; i < facet.getChildCount(); i++) {
-                UIComponent child = facet.getChildren().get(i);
-                if (child instanceof ValueHolder) {
-                    callback.invokeContextCallback(context, child);
-                }
-            }
-        }
-        // the facet contains only one child now, which means that the facet is the child component
-        else {
-            // the child is a ValueHolder, independent if it's a composite or not
-            if (facet instanceof ValueHolder) {
-                callback.invokeContextCallback(context, facet);
+    public static void invokeOnEditableValueHolder(FacesContext context, UIComponent facet, ContextCallback callback) {
+
+        VisitContext visitContext = VisitContext.createVisitContext(context, null,
+                ComponentUtils.VISIT_HINTS_SKIP_UNRENDERED);
+
+        // loop through all facet components
+        // sometimes its not enough to check the first component as it may be wrapped in some layout-components
+        facet.visitTree(visitContext, (ctx, component) -> {
+            // the child is a EditableValueHolder, independent if it's a composite or not
+            if (component instanceof EditableValueHolder) {
+                callback.invokeContextCallback(context, component);
             }
             // try to check for cc:editableValueHolder
-            else if (CompositeUtils.isComposite(facet)) {
-                CompositeUtils.invokeOnDeepestEditableValueHolder(context, facet, callback);
+            else if (CompositeUtils.isComposite(component)) {
+                CompositeUtils.invokeOnDeepestEditableValueHolder(context, component, callback);
             }
-        }
+
+            return VisitResult.ACCEPT;
+        });
     }
 
 }
diff --git a/primefaces/src/main/java/org/primefaces/util/FileUploadUtils.java b/primefaces/src/main/java/org/primefaces/util/FileUploadUtils.java
index a827902416..f06c5591e6 100644
--- a/primefaces/src/main/java/org/primefaces/util/FileUploadUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/FileUploadUtils.java
@@ -24,6 +24,8 @@
 package org.primefaces.util;
 
 import java.io.*;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -102,6 +104,7 @@ public class FileUploadUtils {
             throw validationError("Invalid path: (" + filePath + ") contains unprintable character: " + ch);
         }
 
+        filePath = URLDecoder.decode(filePath, StandardCharsets.UTF_8);
         if (PERCENTS_PAT.matcher(filePath).find()) {
             throw validationError("Invalid path: " + filePath);
         }


Test patch:
diff --git a/primefaces/src/test/java/org/primefaces/util/FileUploadUtilsTest.java b/primefaces/src/test/java/org/primefaces/util/FileUploadUtilsTest.java
index 9ca37091d3..9d9ad244dd 100644
--- a/primefaces/src/test/java/org/primefaces/util/FileUploadUtilsTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/FileUploadUtilsTest.java
@@ -175,6 +175,31 @@ class FileUploadUtilsTest {
         }
     }
 
+    @Test
+    void requireValidFilePath_AbsoluteFileWithWindowsSpaces() {
+        // Unix systems can start with / but Windows cannot
+        String os = System.getProperty("os.name").toLowerCase();
+
+        // Arrange
+        String relativePath = "/D:/Development/OS%20Projects/primefaces/primefaces/target/test-classes/test.png";
+
+        // Act
+        if (os.contains("win")) {
+            try {
+                FileUploadUtils.requireValidFilePath(relativePath, false);
+                fail("File was absolute path and should have failed");
+            }
+            catch (ValidatorException e) {
+                // Assert
+                assertEquals("Invalid directory, name does not match the canonical path.", e.getFacesMessage().getDetail());
+            }
+        }
+        else {
+            String result = FileUploadUtils.requireValidFilePath(relativePath, false);
+            assertTrue(result.endsWith("test.png"));
+        }
+    }
+
     @Test
     void requireValidFilePath_PathTraversal() {
         // Arrange