Repository: primefaces/primefaces (java)

Issue:
DataTable: support read-only fields for filtering/sorting
currently if you define a field, which does not have a setter, a exception will be thrown:

```
java.beans.IntrospectionException: Method not found: setId
	at java.desktop/java.beans.PropertyDescriptor.<init>(PropertyDescriptor.java:114)
	at java.desktop/java.beans.PropertyDescriptor.<init>(PropertyDescriptor.java:74)
	at org.primefaces.util.PropertyDescriptorResolver$DefaultResolver.lambda$getSimpleProperty$1(PropertyDescriptorResolver.java:99)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
	at org.primefaces.util.PropertyDescriptorResolver$DefaultResolver.getSimpleProperty(PropertyDescriptorResolver.java:97)
	at org.primefaces.util.PropertyDescriptorResolver$DefaultResolver.getValue(PropertyDescriptorResolver.java:77)
	at org.primefaces.util.SortTableComparator$FieldMapper.map(SortTableComparator.java:182)
	at org.primefaces.util.SortTableComparator.compareWithMapper(SortTableComparator.java:89)
	at org.primefaces.util.SortTableComparator.compare(SortTableComparator.java:77)
	at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
	at java.base/java.util.TimSort.sort(TimSort.java:220)
	at java.base/java.util.Arrays.sort(Arrays.java:1307)
	at java.base/java.util.ArrayList.sort(ArrayList.java:1721)
	at org.primefaces.model.DefaultLazyDataModel.sort(DefaultLazyDataModel.java:86)
	at org.primefaces.model.DefaultLazyDataModel.load(DefaultLazyDataModel.java:69)
```

currently DataTable only need to read the field and something you dont have the chance to add a setter to a legacy backend code - 
we should support it

Pull request description:
Fix #12354: 14.0.4 DT support read-only fields for filtering/sorting
Fix #12354: 14.0.4 DT support read-only fields for filtering/sorting

Patch:
diff --git a/primefaces/src/main/java/org/primefaces/util/LangUtils.java b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
index 8f4a2b5fa7..f1d4c97505 100644
--- a/primefaces/src/main/java/org/primefaces/util/LangUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
@@ -23,6 +23,8 @@
  */
 package org.primefaces.util;
 
+import static java.util.Locale.ENGLISH;
+
 import java.beans.BeanInfo;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
@@ -484,6 +486,19 @@ public class LangUtils {
         return String.join(" ", values);
     }
 
+    /**
+     * Capitalizes the first character of the given string.
+     *
+     * @param name The string to capitalize.
+     * @return The capitalized string if the input is not blank; otherwise, returns the input string unchanged.
+     */
+    public static String capitalize(String name) {
+        if (isBlank(name)) {
+            return name;
+        }
+        return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1);
+    }
+
 
     /**
      * <p>Checks whether the given String is a parsable number.</p>
diff --git a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
index a1552da866..d6e7f4c5c2 100644
--- a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
+++ b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
@@ -23,12 +23,14 @@
  */
 package org.primefaces.util;
 
+
 import java.beans.IntrospectionException;
 import java.beans.PropertyDescriptor;
 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 {
@@ -99,7 +101,18 @@ public interface PropertyDescriptorResolver {
                     return new PropertyDescriptor(k, klazz);
                 }
                 catch (IntrospectionException e) {
-                    throw new FacesException(e);
+                    // try fallback without write method, our main concern is to read properties here
+                    try {
+                        // "is" + capitalize(k) is actually copied from another PropertyDescriptor constructor
+                        // it will somehow also work for "get" prefix
+                        return new PropertyDescriptor(k,
+                                klazz,
+                                "is" + LangUtils.capitalize(k),
+                                null);
+                    }
+                    catch (IntrospectionException e2) {
+                        throw new FacesException(e2);
+                    }
                 }
             });
         }


Test patch:
diff --git a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
index 5c0beb1e12..8bf271fdcf 100644
--- a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
@@ -23,9 +23,13 @@
  */
 package org.primefaces.util;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.beans.PropertyDescriptor;
+
 import javax.faces.FacesException;
 
 import org.junit.jupiter.api.Test;
@@ -75,6 +79,20 @@ class PropertyDescriptorResolverTest {
         assertThrows(FacesException.class, () -> propResolver.getValue(b, "unknown"));
     }
 
+    @Test
+    void getValueFromReadOnlyPropertyDescriptor() {
+        PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
+
+        F f = new F(true, false, "test");
+        assertEquals(true, propResolver.getValue(f, "readOnly1"));
+        assertEquals(false, propResolver.getValue(f, "readOnly2"));
+        assertEquals("test", propResolver.getValue(f, "readOnly3"));
+
+        assertNull(propResolver.get(f.getClass(), "readOnly1").getWriteMethod());
+        assertNull(propResolver.get(f.getClass(), "readOnly2").getWriteMethod());
+        assertNull(propResolver.get(f.getClass(), "readOnly3").getWriteMethod());
+    }
+
     private abstract class A {
 
         private String name;
@@ -169,4 +187,28 @@ class PropertyDescriptorResolverTest {
         Y,
         Z;
     }
+
+    public class F {
+        private boolean readOnly1;
+        private boolean readOnly2;
+        private String readOnly3;
+
+        public F(boolean readOnly1, boolean readOnly2, String readOnly3) {
+            this.readOnly1 = readOnly1;
+            this.readOnly2 = readOnly2;
+            this.readOnly3 = readOnly3;
+        }
+
+        public boolean isReadOnly1() {
+            return readOnly1;
+        }
+
+        public boolean isReadOnly2() {
+            return readOnly2;
+        }
+
+        public String getReadOnly3() {
+            return readOnly3;
+        }
+    }
 }