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 description:
Fix #12365: 14.0.4 Spinner do not allow ONLY a thousandsSeparator char or any invalid number
Fix #12365: 14.0.4 Spinner do not allow ONLY a thousandsSeparator char or any invalid number

Patch:
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..90418be2dc 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, 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, String.format("Invalid number format: %s", submittedValue));
                 return;
             }
         }
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 b36b8cf291..c4cfc85fa5 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/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;