Repository: primefaces/primefaces (java)

Issue:
Spinner: Exception when entering only thousandSeparator
### Describe the bug

When entering only the thousandSeparator in a non-required spinner, a NumberFormatException is thrown on postback.

### Reproducer

[primefaces-test.zip](https://github.com/user-attachments/files/16454987/primefaces-test.zip)

1. Enter only your thousandSeparator (so probably . for you melloware :wink:)
2. Click the button
3. Exception in the log (would crash an application with proper AJAX error handler)
4. Yes, users are stupid

### Expected behavior

No error occurs and the value NULL is written to the associated field.

### PrimeFaces edition

Elite

### PrimeFaces version

14.0.3

### Theme

_No response_

### JSF implementation

All

### JSF version

4.0

### Java version

17

### Browser(s)

_No response_

Pull request:
Fix #12365: Spinner do not allow ONLY a thousandsSeparator char or any invalid number
Fix #12365: Spinner do not allow ONLY a thousandsSeparator char or any invalid number

- [x] Integration test added

Patch:
diff --git a/docs/15_0_0/components/datatable.md b/docs/15_0_0/components/datatable.md
index 09da1a1fd3..d811699fca 100644
--- a/docs/15_0_0/components/datatable.md
+++ b/docs/15_0_0/components/datatable.md
@@ -190,7 +190,7 @@ public class CarBean {
     @PostConstruct
     public void init() {
         cars = DefaultLazyDataModel.<Car>builder()
-                .valueSupplier(() -> carService.getCars())
+                .valueSupplier((filterBy) -> carService.getCars())
                 .rowKeyProvider(Car::getId) // required for selection
                 .build();
     }
@@ -1104,9 +1104,10 @@ private DefaultLazyDataModel<MyPojo> dataModel;
 @PostConstruct
 public void init() {
     dataModel = DefaultLazyDataModel.<MyPojo>builder()
-        .valueSupplier(() -> service.getListOfMyPojos())
-        .rowKeyProvider(MyPojo::getId) // required for selection
-        .build();
+            .valueSupplier((filterBy) -> service.getListOfMyPojos())
+            .rowKeyProvider(MyPojo::getId) // required for selection
+            .build();
+}
 ```
 
 ## Sticky Header
diff --git a/primefaces/src/main/java/org/primefaces/component/spinner/SpinnerRenderer.java b/primefaces/src/main/java/org/primefaces/component/spinner/SpinnerRenderer.java
index e20e33d5c4..d1a708c5d3 100644
--- a/primefaces/src/main/java/org/primefaces/component/spinner/SpinnerRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/spinner/SpinnerRenderer.java
@@ -67,9 +67,17 @@ public class SpinnerRenderer extends InputRenderer {
                 submittedValue = submittedValue.replace(spinner.getDecimalSeparator(), ".");
             }
 
-            // GitHub #11830 prevent value outside of minimum or maximum range
-            double submittedNumber = Double.parseDouble(submittedValue);
-            if (submittedNumber < spinner.getMin() || submittedNumber > spinner.getMax()) {
+            try {
+                // GitHub #11830 prevent value outside of minimum or maximum range
+                double submittedNumber = Double.parseDouble(submittedValue);
+                if (submittedNumber < spinner.getMin() || submittedNumber > spinner.getMax()) {
+                    logDevelopmentWarning(context, this, String.format("Value is outside min/max range: %s", submittedValue));
+                    return;
+                }
+            }
+            catch (NumberFormatException e) {
+                // GitHub #12365 prevent any invalid number like just the thousands separator
+                logDevelopmentWarning(context, this, String.format("Invalid number format: %s", submittedValue));
                 return;
             }
         }
diff --git a/primefaces/src/main/java/org/primefaces/model/DefaultLazyDataModel.java b/primefaces/src/main/java/org/primefaces/model/DefaultLazyDataModel.java
index aafa78148d..ccccce202f 100644
--- a/primefaces/src/main/java/org/primefaces/model/DefaultLazyDataModel.java
+++ b/primefaces/src/main/java/org/primefaces/model/DefaultLazyDataModel.java
@@ -36,6 +36,13 @@ import org.primefaces.context.PrimeApplicationContext;
 import org.primefaces.model.filter.FilterConstraint;
 import org.primefaces.util.*;
 
+/**
+ * Default implementation of the {@link LazyDataModel}, which implements sorting and filtering via reflection.
+ * It provides some useful features to get more control over filtering,
+ * without lossing the power and performance of the {@link LazyDataModel}.
+ *
+ * @param <T> The result obj
+ */
 public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
 
     private static final Logger LOGGER = Logger.getLogger(DefaultLazyDataModel.class.getName());
@@ -43,9 +50,10 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
     private String rowKeyField;
     private FilterConstraint filter;
     private Sorter<T> sorter;
-    private Callbacks.SerializableSupplier<List<T>> valuesSupplier;
+    private ValuesSupplier<T> valuesSupplier;
     private Callbacks.SerializableFunction<T, Object> rowKeyProvider;
     private Callbacks.SerializablePredicate<T> skipFiltering;
+    private Callbacks.SerializablePredicate<FilterMeta> ignoreFilter;
 
     /**
      * For serialization only
@@ -61,7 +69,7 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
 
     @Override
     public List<T> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
-        List<T> values = valuesSupplier.get();
+        List<T> values = valuesSupplier.get(filterBy);
         List<T> filteredValues = filter(values, filterBy);
 
         setRowCount(filteredValues.size());
@@ -106,6 +114,7 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
 
         return values.stream()
                 .filter(obj -> {
+                    // always include the current obj in the result?
                     if (skipFiltering != null && skipFiltering.test(obj)) {
                         return true;
                     }
@@ -113,6 +122,7 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
                     boolean localMatch = true;
                     boolean globalMatch = false;
 
+                    // global filtering
                     if (hasGlobalFilter) {
                         if (globalFilter != null && globalFilter.isActive()) {
                             globalMatch = globalFilter.getConstraint().isMatching(context, obj, globalFilter.getFilterValue(), locale);
@@ -122,11 +132,18 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
                         }
                     }
 
+                    // local filtering
                     for (FilterMeta filterMeta : filterBy.values()) {
+                        // skip filter
                         if (filterMeta.getField() == null || filterMeta.getFilterValue() == null || filterMeta.isGlobalFilter()) {
                             continue;
                         }
 
+                        // ignore this filter?
+                        if (ignoreFilter != null && ignoreFilter.test(filterMeta)) {
+                            continue;
+                        }
+
                         Object fieldValue = propResolver.getValue(obj, filterMeta.getField());
                         Object filterValue = filterMeta.getFilterValue();
                         Object convertedFilterValue = null;
@@ -158,7 +175,7 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
 
     @Override
     public T getRowData(String rowKey) {
-        List<T> values = Objects.requireNonNullElseGet(valuesSupplier.get(), Collections::emptyList);
+        List<T> values = Objects.requireNonNullElseGet(valuesSupplier.get(Collections.emptyMap()), Collections::emptyList);
         for (T obj : values) {
             if (Objects.equals(rowKey, getRowKey(obj))) {
                 return obj;
@@ -184,7 +201,7 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
             model = new DefaultLazyDataModel<>();
         }
 
-        public Builder<T> valueSupplier(Callbacks.SerializableSupplier<List<T>> valuesSupplier) {
+        public Builder<T> valueSupplier(ValuesSupplier<T> valuesSupplier) {
             model.valuesSupplier = valuesSupplier;
             return this;
         }
@@ -209,11 +226,30 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
             return this;
         }
 
+        /**
+         * Registers a callback that can decide whether to always include the given object in the result,
+         * independent of the current filters.
+         *
+         * @param skipFiltering the callback
+         * @return the current builder
+         */
         public Builder<T> skipFiltering(Callbacks.SerializablePredicate<T> skipFiltering) {
             model.skipFiltering = skipFiltering;
             return this;
         }
 
+        /**
+         * Registers a callback that can decide to ignore the given filter completely.
+         * You may manually respect the filter in the {@link #valuesSupplier}.
+         *
+         * @param ignoreFilter the callback
+         * @return the current builder
+         */
+        public Builder<T> ignoreFilter(Callbacks.SerializablePredicate<FilterMeta> ignoreFilter) {
+            model.ignoreFilter = ignoreFilter;
+            return this;
+        }
+
         public Builder<T> sorter(Sorter<T> sorter) {
             model.sorter = sorter;
             return this;
@@ -243,4 +279,10 @@ public class DefaultLazyDataModel<T> extends LazyDataModel<T> {
     public interface Sorter<T> extends Comparator<T>, Serializable {
 
     }
+
+    @FunctionalInterface
+    public interface ValuesSupplier<T> extends Serializable {
+
+        List<T> get(Map<String, FilterMeta> filterBy);
+    }
 }
diff --git a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
index d6e7f4c5c2..455ddf70f1 100644
--- a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
+++ b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
@@ -23,21 +23,22 @@
  */
 package org.primefaces.util;
 
-
+import javax.faces.FacesException;
 import java.beans.IntrospectionException;
 import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
 
-import javax.faces.FacesException;
-
 public interface PropertyDescriptorResolver {
 
-    PropertyDescriptor get(Class<?> klazz, String name);
+    PropertyDescriptor get(Class<?> klazz, String expression);
 
-    Object getValue(Object bean, String name);
+    Object getValue(Object obj, String expression);
+
+    void setValue(Object obj, String expression, Object value);
 
     void flush();
 
@@ -64,8 +65,8 @@ public interface PropertyDescriptorResolver {
             PropertyDescriptor pd = null;
             Class<?> parent = klazz;
 
-            for (String field : NESTED_EXPRESSION_PATTERN.split(expression)) {
-                pd = getSimpleProperty(parent, field);
+            for (String property : NESTED_EXPRESSION_PATTERN.split(expression)) {
+                pd = getSimpleProperty(parent, property);
                 parent = pd.getPropertyType();
             }
 
@@ -73,16 +74,45 @@ public interface PropertyDescriptorResolver {
         }
 
         @Override
-        public Object getValue(Object bean, String expression) {
+        public Object getValue(Object obj, String expression) {
             try {
-                for (String field : NESTED_EXPRESSION_PATTERN.split(expression)) {
-                    bean = getSimpleProperty(bean.getClass(), field).getReadMethod().invoke(bean);
-                    if (bean == null) {
+                for (String property : NESTED_EXPRESSION_PATTERN.split(expression)) {
+                    obj = getSimpleProperty(obj.getClass(), property).getReadMethod().invoke(obj);
+                    if (obj == null) {
                         break;
                     }
                 }
 
-                return bean;
+                return obj;
+            }
+            catch (ReflectiveOperationException e) {
+                throw new FacesException(e);
+            }
+        }
+
+        @Override
+        public void setValue(Object obj, String expression, Object value) {
+            try {
+                String[] expressions = NESTED_EXPRESSION_PATTERN.split(expression);
+
+                for (int i = 0; i < expressions.length; i++) {
+                    String property = expressions[i];
+                    boolean lastExpression = i + 1 == expressions.length;
+
+                    if (lastExpression) {
+                        Method writeMethod = getSimpleProperty(obj.getClass(), property).getWriteMethod();
+                        if (writeMethod == null) {
+                            throw new FacesException("property '" + property + "' is readonly");
+                        }
+                        writeMethod.invoke(obj, value);
+                    }
+                    else {
+                        obj = getSimpleProperty(obj.getClass(), property).getReadMethod().invoke(obj);
+                        if (obj == null) {
+                            throw new FacesException("property '" + property + "' resolved to null");
+                        }
+                    }
+                }
             }
             catch (ReflectiveOperationException e) {
                 throw new FacesException(e);
@@ -94,9 +124,9 @@ public interface PropertyDescriptorResolver {
             pdCache.clear();
         }
 
-        private PropertyDescriptor getSimpleProperty(Class<?> klazz, String field) {
+        private PropertyDescriptor getSimpleProperty(Class<?> klazz, String property) {
             String cacheKey = klazz.getName();
-            return pdCache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).computeIfAbsent(field, k -> {
+            return pdCache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).computeIfAbsent(property, k -> {
                 try {
                     return new PropertyDescriptor(k, klazz);
                 }
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
index f5e80795f7..51a65c70a4 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
@@ -2214,9 +2214,16 @@
             if (this.documentClickListener) {
                 this.datepickerClick = true;
             }
-            // #11928 allow the input to be clicked again to close panel and allow typing of date
-            if (!this.datepickerFocus && this.isPanelVisible()) {
-                this.hideOverlay();
+            
+            if (this.isPanelVisible()) {
+                // #11928 allow the input to be clicked again to close panel and allow typing of date
+                if (!this.datepickerFocus) {
+                    this.hideOverlay();
+                }
+            } 
+            else if (this.options.showOnFocus) {
+                // #12361 allow the input to be clicked again to open the panel if showOnFocus is true
+                this.showOverlay();
             }
         },
 


Test patch:
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable001.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable001.java
index 10a542b64d..a9fea38dbb 100644
--- a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable001.java
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable001.java
@@ -56,7 +56,7 @@ public class DataTable001 implements Serializable {
     public void init() {
         progLanguages = service.getLangs();
         progLanguagesLazyDataModel = DefaultLazyDataModel.<ProgrammingLanguage>builder()
-                .valueSupplier(() -> service.getLangs())
+                .valueSupplier((filterBy) -> service.getLangs())
                 .rowKeyProvider(ProgrammingLanguage::getId)
                 .build();
         globalFilterOnly = false;
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable002.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable002.java
index ff2739ee01..6d1d3bee87 100644
--- a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable002.java
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable002.java
@@ -56,7 +56,7 @@ public class DataTable002 implements Serializable {
         }
         lazyDataModel = new ProgrammingLanguageLazyDataModel();
         reflectionLazyDataModel = DefaultLazyDataModel.<ProgrammingLanguage>builder()
-                .valueSupplier(() -> programmingLanguages)
+                .valueSupplier((filterBy) -> programmingLanguages)
                 .rowKeyProvider(ProgrammingLanguage::getId)
                 .build();
     }
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable044.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable044.java
index e61a620214..50747212aa 100644
--- a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable044.java
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/datatable/DataTable044.java
@@ -45,7 +45,7 @@ public class DataTable044 implements Serializable {
     @PostConstruct
     public void init() {
         lazyDataModel = DefaultLazyDataModel.<ProgrammingLanguage>builder()
-                .valueSupplier(service::getLangs)
+                .valueSupplier((filterBy) -> service.getLangs())
                 .filter((ctx, value, filter, locale) -> {
                     if (programmingLanguageFilter == null) {
                         return true;
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/spinner/Spinner001Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/spinner/Spinner001Test.java
index 406364ed27..8333eae5da 100644
--- a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/spinner/Spinner001Test.java
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/spinner/Spinner001Test.java
@@ -125,6 +125,23 @@ public class Spinner001Test extends AbstractPrimePageTest {
         assertConfiguration(spinner.getWidgetConfiguration());
     }
 
+    @Test
+    @Order(6)
+    @DisplayName("Spinner: GitHub #12365 do not allow only a thousands separator character")
+    void thousandSeparatorOnly(Page page) {
+        // Arrange
+        Spinner spinner = page.spinner;
+        assertEquals("", spinner.getValue());
+
+        // Act
+        sendKeys(spinner, ",");
+        page.button.click();
+
+        // Assert
+        assertEquals("", spinner.getValue());
+        assertConfiguration(spinner.getWidgetConfiguration());
+    }
+
     private void assertConfiguration(JSONObject cfg) {
         assertNoJavascriptErrors();
         System.out.println("Spinner Config = " + cfg);
diff --git a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
index 8bf271fdcf..c5f1d2d3fd 100644
--- a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
@@ -37,7 +37,7 @@ import org.junit.jupiter.api.Test;
 class PropertyDescriptorResolverTest {
 
     @Test
-    void getPropertyDescriptor() {
+    void get() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         PropertyDescriptor pd = propResolver.get(A.class, "name");
@@ -60,7 +60,7 @@ class PropertyDescriptorResolverTest {
     }
 
     @Test
-    void getValueFromPropertyDescriptor() {
+    void getValue() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         B b = new B("b", true, Boolean.FALSE, DummyEnum.X);
@@ -80,7 +80,7 @@ class PropertyDescriptorResolverTest {
     }
 
     @Test
-    void getValueFromReadOnlyPropertyDescriptor() {
+    void getValueReadOnly() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         F f = new F(true, false, "test");
@@ -93,7 +93,27 @@ class PropertyDescriptorResolverTest {
         assertNull(propResolver.get(f.getClass(), "readOnly3").getWriteMethod());
     }
 
-    private abstract class A {
+    @Test
+    void setValue() {
+        PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
+
+        A a = new A(null, false, false);
+        assertNull(propResolver.getValue(a, "name"));
+        propResolver.setValue(a, "name", "Hans");
+        assertEquals("Hans", propResolver.getValue(a, "name"));
+
+        B b = new B("b", true, Boolean.FALSE, DummyEnum.X);
+        D d = new D("d", false, Boolean.FALSE, DummyEnum.Z, 123);
+        C c = new C(d);
+
+        assertEquals(123, propResolver.getValue(c, "b.foo"));
+        propResolver.setValue(c, "b.foo", 124);
+        assertEquals(124, propResolver.getValue(c, "b.foo"));
+
+        assertThrows(FacesException.class, () -> propResolver.setValue(new C(null), "b.dummyEnum", null));
+    }
+
+    private class A {
 
         private String name;
         private boolean bool1;