Repository: primefaces/primefaces (java)

Issue:
DataTable/TreeTable: New `filterNormalize` attribute
### Description

Firstly I really appreciate the new `filterNormalize="true"` attribute, available in components like `<p:selectCheckboxMenu/>`
 
 **I was wondering if it is possible to have the same behaviour in _<p:datatable columns_ /> as well.** E.g.:
`<p:column   filterBy="#{op.name}"   filterMatchMode="contains"  **filterNormalize="true"**   .../> `

BTW, I'm actually using a custom filter method in Bean and using it like that: `filterFunction="#{utilBean.filterContainsWithoutAccents}"` and it works fine when user filter using the Column Header Filter Box, but fails when user use the main global filter box like:
` <h:inputText id="globalFilter"  onkeydown="if (event.keyCode == 13) { return  PF('tableWdg').filter(); }"        />` which not calls ServerSide Bean method` public boolean filterContainsWithoutAccents(Object value, Object filter, Locale locale) {.`.. anymore.

Regards 

### Describe the solution you would like

For me, the cleanest solution would be  to have  `filterNormalize=""` attribute like we have now in components like `<p:selectCheckboxMenu/>` to filtering for column data, escaping accented characters.

As a workaround I'm using a server side custom filter method called in: `filterFunction="#{utilBean.filterContainsWithoutAccents}"`  but apparently it just work   when called by the 'Column Header Filter Box', and it's not   called   by global filter `PF('tableWdg').filter();`,  there is a way to call all declared columns `filterFunction=""` in that context as well?

Thanks in advance.

### Additional context

_No response_

Pull request description:
Fix #12511: Datatable/TreeTable filterNormalize
Fix #12511: Datatable/TreeTable filterNormalize

@jepsar this is what I was thinking because its easier to capture here on the server side no matter what filters or global filters in play.

Patch:
diff --git a/docs/15_0_0/components/datatable.md b/docs/15_0_0/components/datatable.md
index d02655a5ff..6ed629f464 100644
--- a/docs/15_0_0/components/datatable.md
+++ b/docs/15_0_0/components/datatable.md
@@ -46,6 +46,7 @@ DataTable displays data in tabular format.
 | filterDelay               | 300                | Integer          | Delay in milliseconds before sending an ajax filter query.
 | filterEvent               | keyup              | String           | Event triggering filter for input filters. If "enter" it will only filter after ENTER key is pressed.
 | filteredValue             | null               | List             | List to keep filtered data.
+| filterNormalize           | false              | Boolean          | Defines if filtering would be done using normalized values (accents will be removed from characters). Default is false.
 | first                     | 0                  | Integer          | Index of the first row to be displayed
 | frozenColumns             | 0                  | Integer          | Number of columns to freeze from start index 0.
 | frozenColumnsAlignment    | left               | String           | Defines Alignment of frozen columns, valid values are `left` (default) and `right` 
diff --git a/docs/15_0_0/components/treetable.md b/docs/15_0_0/components/treetable.md
index 89daffebc0..be4be2bf4a 100644
--- a/docs/15_0_0/components/treetable.md
+++ b/docs/15_0_0/components/treetable.md
@@ -38,6 +38,7 @@ filterBy | null | SortMeta / Collection<SortMeta> | Property to be used for defa
 filterDelay | 300 | Integer | Delay in milliseconds before sending an ajax filter query.
 filterEvent | keyup | String | Event triggering filter for input filters. If "enter" it will only filter after ENTER key is pressed.
 filteredValue  | null | TreeNode | TreeNode to keep filtered data.
+filterNormalize | false | Boolean | Defines if filtering would be done using normalized values (accents will be removed from characters). Default is false.
 first | 0 | Integer | Index of the first data to display.
 globalFilter | null | String | Value of the global filter to use when filtering by default.
 globalFilterFunction | null | MethodExpression | Custom implementation to globally filter a value against a constraint.
diff --git a/docs/15_0_0/gettingstarted/whatsnew.md b/docs/15_0_0/gettingstarted/whatsnew.md
index 261e399d87..484ad6e763 100644
--- a/docs/15_0_0/gettingstarted/whatsnew.md
+++ b/docs/15_0_0/gettingstarted/whatsnew.md
@@ -33,6 +33,7 @@ Look into [migration guide](https://primefaces.github.io/primefaces/15_0_0/#/../
     * Added attribute `frozenColumnsAlignment` to support alignment of frozen columns left and right
     * Added `expandIcon`/`collapseIcon` attributes for RowToggler
     * Added `rowsPerPage` in the PageEvent AJAX event
+    * Added `filterNormalize` attribute to normalize the filter values (remove accents)
 
 * DatePicker
     * Added `defaultHour`, `defaultMinute`, `defaultSecond`, `defaultMillisec` attributes to match legacy `Calendar` component
@@ -61,4 +62,7 @@ Look into [migration guide](https://primefaces.github.io/primefaces/15_0_0/#/../
 * Tree
     * Added `filterPlaceholder` property
 
+* TreeTable
+    * Added `filterNormalize` attribute to normalize the filter values (remove accents)
+
     
\ No newline at end of file
diff --git a/docs/migrationguide/15_0_0.md b/docs/migrationguide/15_0_0.md
index c3b6fc76a9..c341389e6b 100644
--- a/docs/migrationguide/15_0_0.md
+++ b/docs/migrationguide/15_0_0.md
@@ -11,10 +11,14 @@
 ## Deprecated
 
   * `Separator` component is deprecated since 15.0.0, please switch to `Divider`.
-  * `Spacer` component is deprecated since 15.0.0, please use any of the modern CSS methods of spacing.
+  * `Spacer` component is deprecated since 15.0.0, please use any of the modern CSS methods of spacing. We are not deleting it as it's been around since PF 2.0 and used widely in applications.
   * The `allSuffix` attribute of the `importEnum` is deprecated since 15.0.0. Please use the special key `ALL_VALUES` to
     access all values, which is and was always available.
 
+## FilterMeta
+
+  * `FilterMeta` has a new `normalize` property which is read from the Datatable/TreeTable `isFilterNormalize` property to handle diacritics.
+
 ## Core
 
   * Class `org.primefaces.component.api.UIData` was renamed `org.primefaces.component.api.PrimeUIData`.
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 49f3352377..f74223e219 100644
--- a/primefaces/src/main/java/org/primefaces/component/api/UITable.java
+++ b/primefaces/src/main/java/org/primefaces/component/api/UITable.java
@@ -76,7 +76,7 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
 
         // build columns filterBy
         forEachColumn(c -> {
-            FilterMeta meta = FilterMeta.of(context, getVar(), c);
+            FilterMeta meta = FilterMeta.of(context, getVar(), c, isFilterNormalize());
             if (meta != null) {
                 filterBy.put(meta.getColumnKey(), meta);
             }
@@ -161,7 +161,7 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
             if (globalFilterComponent instanceof ValueHolder) {
                 ((ValueHolder) globalFilterComponent).setValue(globalFilterDefaultValue);
             }
-            FilterMeta globalFilterBy = FilterMeta.of(globalFilterDefaultValue, getGlobalFilterFunction());
+            FilterMeta globalFilterBy = FilterMeta.of(globalFilterDefaultValue, getGlobalFilterFunction(), isFilterNormalize());
             filterBy.put(globalFilterBy.getColumnKey(), globalFilterBy);
         }
     }
@@ -173,7 +173,7 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
         }
 
         // lazy init - happens in cases where the column is initially not rendered
-        FilterMeta f = FilterMeta.of(context, getVar(), column);
+        FilterMeta f = FilterMeta.of(context, getVar(), column, isFilterNormalize());
         if (f != null) {
             filterBy.put(f.getColumnKey(), f);
         }
@@ -281,6 +281,8 @@ public interface UITable<T extends UITableState> extends ColumnAware, MultiViewS
 
     void setGlobalFilterOnly(boolean globalFilterOnly);
 
+    boolean isFilterNormalize();
+
     default Map<String, SortMeta> initSortBy(FacesContext context) {
         Map<String, SortMeta> sortBy = new LinkedHashMap<>();
         AtomicBoolean sorted = new AtomicBoolean();
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
index 5f0887f461..1c89cb1a05 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
@@ -71,6 +71,7 @@ public abstract class DataTableBase extends UIPageableData implements Widget, RT
         filterBy,
         filterDelay,
         filterEvent,
+        filterNormalize,
         filteredValue,
         frozenColumns,
         frozenColumnsAlignment,
@@ -790,4 +791,13 @@ public abstract class DataTableBase extends UIPageableData implements Widget, RT
     public void setCellNavigation(boolean cellNavigation) {
         getStateHelper().put(PropertyKeys.cellNavigation, cellNavigation);
     }
+
+    @Override
+    public boolean isFilterNormalize() {
+        return (Boolean) getStateHelper().eval(PropertyKeys.filterNormalize, false);
+    }
+
+    public void setFilterNormalize(boolean filterNormalize) {
+        getStateHelper().put(PropertyKeys.filterNormalize, filterNormalize);
+    }
 }
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/feature/FilterFeature.java b/primefaces/src/main/java/org/primefaces/component/datatable/feature/FilterFeature.java
index 883ffd4978..1e328a4de6 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/feature/FilterFeature.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/feature/FilterFeature.java
@@ -174,4 +174,4 @@ public class FilterFeature implements DataTableFeature {
         table.setValue(DataTable.convertIntoObjectValueType(context, table, filtered));
         table.setRowIndex(-1); //reset datamodel
     }
-}
+}
\ No newline at end of file
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableBase.java b/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableBase.java
index 0acb2b398b..b8886533fe 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableBase.java
@@ -77,6 +77,7 @@ public abstract class TreeTableBase extends UITree implements Widget, ClientBeha
         rows,
         first,
         filterBy,
+        filterNormalize,
         globalFilter,
         globalFilterFunction,
         globalFilterOnly,
@@ -531,4 +532,13 @@ public abstract class TreeTableBase extends UITree implements Widget, ClientBeha
     public void setExportTag(String exportTag) {
         getStateHelper().put(PropertyKeys.exportTag, exportTag);
     }
+
+    @Override
+    public boolean isFilterNormalize() {
+        return (Boolean) getStateHelper().eval(PropertyKeys.filterNormalize, false);
+    }
+
+    public void setFilterNormalize(boolean filterNormalize) {
+        getStateHelper().put(PropertyKeys.filterNormalize, filterNormalize);
+    }
 }
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/feature/FilterFeature.java b/primefaces/src/main/java/org/primefaces/component/treetable/feature/FilterFeature.java
index 47d0b3b8c4..72ef6e1550 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/feature/FilterFeature.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/feature/FilterFeature.java
@@ -285,4 +285,4 @@ public class FilterFeature implements TreeTableFeature {
         return context.getExternalContext().getRequestParameterMap().containsKey(table.getClientId(context) + "_filtering");
     }
 
-}
+}
\ No newline at end of file
diff --git a/primefaces/src/main/java/org/primefaces/model/FilterMeta.java b/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
index 75d65997ec..c2f1655743 100644
--- a/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
+++ b/primefaces/src/main/java/org/primefaces/model/FilterMeta.java
@@ -55,22 +55,24 @@ public class FilterMeta implements Serializable {
     private Object filterValue; // should be null if empty string/collection/array/object
     private MatchMode matchMode = MatchMode.CONTAINS;
     private FilterConstraint constraint;
+    private boolean normalize = false;
 
     public FilterMeta() {
         // NOOP
     }
 
     FilterMeta(String columnKey, String field, FilterConstraint constraint,
-               ValueExpression filterBy, Object filterValue, MatchMode matchMode) {
+               ValueExpression filterBy, Object filterValue, MatchMode matchMode, boolean normalize) {
         this.field = field;
         this.columnKey = columnKey;
         this.filterBy = filterBy;
         this.constraint = constraint;
-        this.filterValue = resetToNullIfEmpty(filterValue);
         this.matchMode = matchMode;
+        this.normalize = normalize;
+        setFilterValue(filterValue);
     }
 
-    public static FilterMeta of(FacesContext context, String var, UIColumn column) {
+    public static FilterMeta of(FacesContext context, String var, UIColumn column, boolean normalize) {
         if (column instanceof DynamicColumn) {
             ((DynamicColumn) column).applyStatelessModel();
         }
@@ -106,16 +108,18 @@ public class FilterMeta implements Serializable {
                 filterValue = state.getValue();
             }
         }
+        filterValue = LangUtils.normalize(filterValue, normalize);
 
         return new FilterMeta(column.getColumnKey(),
                               field,
                               constraint,
                               filterByVE,
                               filterValue,
-                              matchMode);
+                              matchMode,
+                              normalize);
     }
 
-    public static FilterMeta of(Object globalFilterValue, MethodExpression globalFilterFunction) {
+    public static FilterMeta of(Object globalFilterValue, MethodExpression globalFilterFunction, boolean normalize) {
         FilterConstraint constraint = globalFilterFunction == null
                 ? new GlobalFilterConstraint()
                 : new FunctionFilterConstraint(globalFilterFunction);
@@ -124,8 +128,9 @@ public class FilterMeta implements Serializable {
                               GLOBAL_FILTER_KEY,
                               constraint,
                               null,
-                              globalFilterValue,
-                              MatchMode.GLOBAL);
+                              LangUtils.normalize(globalFilterValue, normalize),
+                              MatchMode.GLOBAL,
+                              normalize);
     }
 
     public static <T> T resetToNullIfEmpty(T filterValue) {
@@ -160,7 +165,7 @@ public class FilterMeta implements Serializable {
     }
 
     public void setFilterValue(Object filterValue) {
-        this.filterValue = resetToNullIfEmpty(filterValue);
+        this.filterValue = resetToNullIfEmpty(LangUtils.normalize(filterValue, isNormalize()));
     }
 
     public FilterConstraint getConstraint() {
@@ -191,7 +196,15 @@ public class FilterMeta implements Serializable {
         if (column instanceof DynamicColumn) {
             ((DynamicColumn) column).applyStatelessModel();
         }
-        return filterBy.getValue(elContext);
+        return LangUtils.normalize(filterBy.getValue(elContext), isNormalize());
+    }
+
+    public boolean isNormalize() {
+        return normalize;
+    }
+
+    public void setNormalize(boolean normalize) {
+        this.normalize = normalize;
     }
 
     public static Builder builder() {
@@ -231,6 +244,11 @@ public class FilterMeta implements Serializable {
             return this;
         }
 
+        public Builder normalize(boolean normalize) {
+            filterBy.normalize = normalize;
+            return this;
+        }
+
         public FilterMeta build() {
             if (filterBy.matchMode != null) {
                 filterBy.constraint = FilterConstraints.of(filterBy.matchMode);
@@ -251,6 +269,7 @@ public class FilterMeta implements Serializable {
                 ", filterValue=" + filterValue +
                 ", matchMode=" + matchMode +
                 ", constraint=" + constraint +
+                ", normalize=" + normalize +
                 '}';
     }
 
diff --git a/primefaces/src/main/java/org/primefaces/util/LangUtils.java b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
index ea0b228d8c..6c62b5c95d 100644
--- a/primefaces/src/main/java/org/primefaces/util/LangUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
@@ -538,6 +538,26 @@ public class LangUtils {
         return withDecimalsParsing(str, 0);
     }
 
+    /**
+     * Normalizes the given value by removing diacritics.
+     *
+     * @param value the value to normalize, expected to be a String.
+     * @param shouldNormalize whether to remove diacritics from the string.
+     * @return the normalized string, or an empty string if the input is not a string or is empty.
+     */
+    public static Object normalize(Object value, boolean shouldNormalize) {
+        if (!shouldNormalize || !(value instanceof String)) {
+            return value;
+        }
+
+        String strValue = (String) value;
+        if (strValue.isEmpty()) {
+            return Constants.EMPTY_STRING;
+        }
+
+        return java.text.Normalizer.normalize(strValue, java.text.Normalizer.Form.NFD).replaceAll("\\p{M}", Constants.EMPTY_STRING);
+    }
+
     private static boolean withDecimalsParsing(final String str, final int beginIdx) {
         int decimalPoints = 0;
         for (int i = beginIdx; i < str.length(); i++) {
diff --git a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
index 605e23a39c..d4ca7679b2 100644
--- a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
+++ b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
@@ -8673,6 +8673,14 @@
             <required>false</required>
             <type>java.lang.String</type>
         </attribute>
+        <attribute>
+            <description>
+                <![CDATA[Defines if filtering would be done using normalized values (accents will be removed from characters). Default is false.]]>
+            </description>
+            <name>filterNormalize</name>
+            <required>false</required>
+            <type>java.lang.Boolean</type>
+        </attribute>
         <attribute>
             <description>
                 <![CDATA[Delay to wait in milliseconds before sending each filter query. Default is 300.]]>
@@ -30659,6 +30667,14 @@
             <required>false</required>
             <type>java.lang.String</type>
         </attribute>
+        <attribute>
+            <description>
+                <![CDATA[Defines if filtering would be done using normalized values (accents will be removed from characters). Default is false.]]>
+            </description>
+            <name>filterNormalize</name>
+            <required>false</required>
+            <type>java.lang.Boolean</type>
+        </attribute>
         <attribute>
             <description>
                 <![CDATA[Delay to wait in milliseconds before sending each filter query. Default is 300.]]>


Test patch:
diff --git a/primefaces/src/test/java/org/primefaces/util/LangUtilsTest.java b/primefaces/src/test/java/org/primefaces/util/LangUtilsTest.java
index 5227801146..2dea186819 100644
--- a/primefaces/src/test/java/org/primefaces/util/LangUtilsTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/LangUtilsTest.java
@@ -120,6 +120,31 @@ class LangUtilsTest {
         assertEquals("My Über String", LangUtils.toCapitalCase("myÜberString"));
     }
 
+    @Test
+    void normalize() {
+        assertEquals("test", LangUtils.normalize("test", false));
+        assertEquals("test", LangUtils.normalize("test", true));
+        assertEquals("TEST", LangUtils.normalize("TEST", false));
+        assertEquals("TEST", LangUtils.normalize("TEST", true));
+        assertEquals("tést", LangUtils.normalize("tést", false));
+        assertEquals("test", LangUtils.normalize("tést", true));
+        assertEquals("t e s t", LangUtils.normalize("t e s t", false));
+        assertEquals("t e s t", LangUtils.normalize("t e s t", true));
+        assertEquals("", LangUtils.normalize("", false));
+        assertEquals("", LangUtils.normalize("", true));
+        assertEquals(Constants.EMPTY_STRING, LangUtils.normalize("", true));
+        assertEquals("123", LangUtils.normalize("123", false));
+        assertEquals("123", LangUtils.normalize("123", true));
+        assertEquals("123", LangUtils.normalize("123", false));
+        assertEquals("123", LangUtils.normalize("123", true));
+        assertEquals("test123", LangUtils.normalize("test123", false));
+        assertEquals("test123", LangUtils.normalize("test123", true));
+        assertEquals("tést123", LangUtils.normalize("tést123", false));
+        assertEquals("test123", LangUtils.normalize("tést123", true));
+        SimpleClass simpleClass = new SimpleClass();
+        assertEquals(simpleClass, LangUtils.normalize(simpleClass, true));
+    }
+
     class SimpleClass {
 
         private List<String> strings;