Repository: primefaces/primefaces (java)

Issue:
SelectOneRadio: support readonly
## 1) Environment
- PrimeFaces version: 6.2
- Does it work on the newest released PrimeFaces version? Not with 6.2
- Application server + version: Tomcat 8.0
- Affected browsers: Firefox 63.0.1

## 2) Expected behavior
When I set `readonly="true"` on a `p:selectOneRadio` with `layout="custom"` I expect radio buttons to be rendered as read-only (not clickable).

## 3) Actual behavior
When I set `readonly="true"` on a `p:selectOneRadio` with `layout="custom"` radio buttons are clickable, although some kind of JavaScript code is called after my click to restore the original selection state of buttons. So, even if, in the end, the selectOneRadio value cannot be changed, the current implementation is quite confusing for the end user  (it seems like the interface is joking on him/her...)

Please note that setting `readonly="true"` on all radio buttons as well seems to have no effect.

## 4) Steps to reproduce
Create a  `p:selectOneRadio` with `layout="custom"` and `readonly="true"` and a couple of radio buttons.

## 5) Sample XHTML

```xhtml
<p:selectOneRadio id="myRadioButton"
  value="#{myBean.value}" layout="custom" readonly="true">
  <f:selectItem itemValue="true" />
  <f:selectItem itemValue="false" />
 </p:selectOneRadio>
<p:radioButton id="trueRB" for="myRadioButton" itemIndex="0" /><p:outputLabel for="trueRB" value="True!" /><br/>
<p:radioButton id="falseRB" for="myRadioButton" itemIndex="1" /><p:outputLabel for="falseRB" valure="False!" />
```

## 6) Sample bean
```java
public class MyBean {
  private boolean value = true;
  // setter and getter omitted
}
```

Pull request:
Fix #4238: SelectOneRadio: support readonly
Fix #4238

Patch:
diff --git a/docs/12_0_0/components/selectoneradio.md b/docs/12_0_0/components/selectoneradio.md
index cd8b58699b..969a8f08de 100644
--- a/docs/12_0_0/components/selectoneradio.md
+++ b/docs/12_0_0/components/selectoneradio.md
@@ -43,7 +43,8 @@ styleClass | null | String | Style class of the container.
 tabindex | null | String | Specifies the tab order of element in tab navigation.
 plain | false | Boolean | Plain mode displays radiobuttons using native browser rendering instead of themes.
 unselectable | false | Boolean | Unselectable mode when true clicking a radio again will clear the selection. Default false.
-hideNoSelectionOption | false | boolean  | Flag indicating that, if this component is activated by the user, The "no selection option", if any, must be hidden.
+hideNoSelectionOption | false | Boolean  | Flag indicating that, if this component is activated by the user, The "no selection option", if any, must be hidden.
+readonly | false | Boolean | Flag indicating that this input element will prevent changes by the user.
 
 ## Getting started with SelectOneRadio
 SelectOneRadio usage is same as the standard one.
diff --git a/pom.xml b/pom.xml
index c265e4004a..412383eb3e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,7 +98,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.9.0</version>
+                <version>3.10.0</version>
                 <configuration>
                     <encoding>${project.build.sourceEncoding}</encoding>
                 </configuration>
@@ -143,7 +143,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.3.1</version>
+                <version>3.3.2</version>
                 <configuration>
                     <sourcepath>${basedir}/src/main/java;</sourcepath>
                     <doclint>none</doclint>
@@ -196,7 +196,7 @@
             <plugin>
                 <groupId>org.sonatype.plugins</groupId>
                 <artifactId>nexus-staging-maven-plugin</artifactId>
-                <version>1.6.8</version>
+                <version>1.6.10</version>
                 <extensions>true</extensions>
                 <configuration>
                     <serverId>ossrh</serverId>
diff --git a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/SelectOneRadio.java b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/SelectOneRadio.java
index 0b21f649ce..7535bae686 100644
--- a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/SelectOneRadio.java
+++ b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/SelectOneRadio.java
@@ -170,4 +170,13 @@ public abstract class SelectOneRadio extends AbstractComponent {
     public void enableOption(int index) {
         PrimeSelenium.executeScript(getWidgetByIdScript() + ".enable(" + index + ");");
     }
+
+    /**
+     * Is this component readonly?
+     *
+     * @return true if readonly
+     */
+    public boolean isReadOnly() {
+        return PrimeSelenium.executeScript("return " + getWidgetByIdScript() + ".cfg.readonly");
+    }
 }
diff --git a/primefaces-selenium/primefaces-selenium-core/pom.xml b/primefaces-selenium/primefaces-selenium-core/pom.xml
index 014112ded8..2dc75f91a1 100644
--- a/primefaces-selenium/primefaces-selenium-core/pom.xml
+++ b/primefaces-selenium/primefaces-selenium-core/pom.xml
@@ -82,7 +82,7 @@
         <dependency>
             <groupId>net.bytebuddy</groupId>
             <artifactId>byte-buddy</artifactId>
-            <version>1.12.7</version>
+            <version>1.12.8</version>
         </dependency>
     </dependencies>
 </project>
diff --git a/primefaces-showcase/pom.xml b/primefaces-showcase/pom.xml
index d626d2a14c..0785421339 100644
--- a/primefaces-showcase/pom.xml
+++ b/primefaces-showcase/pom.xml
@@ -18,7 +18,7 @@
 
     <properties>
         <owb.version>2.0.26</owb.version>
-        <hibernate-validator.version>6.2.1.Final</hibernate-validator.version>
+        <hibernate-validator.version>6.2.2.Final</hibernate-validator.version>
         <mojarra.version>2.3.17</mojarra.version>
         <myfaces.version>2.3.9</myfaces.version>
         <myfaces-next.version>2.3-next-M6</myfaces-next.version>
diff --git a/primefaces-showcase/src/main/webapp/ui/input/oneRadio.xhtml b/primefaces-showcase/src/main/webapp/ui/input/oneRadio.xhtml
index 7b43c41b55..467df3f6a4 100644
--- a/primefaces-showcase/src/main/webapp/ui/input/oneRadio.xhtml
+++ b/primefaces-showcase/src/main/webapp/ui/input/oneRadio.xhtml
@@ -35,7 +35,7 @@
         <div class="card">
             <h:form>
                 <h5 class="p-mt-0">Basic</h5>
-                <p:selectOneRadio id="console" value="#{radioView.console}" unselectable="true">
+                <p:selectOneRadio id="console" value="#{radioView.console}">
                     <f:selectItem itemLabel="Option1" itemValue="Option1"/>
                     <f:selectItem itemLabel="Option2" itemValue="Option2"/>
                     <f:selectItem itemLabel="Option3" itemValue="Option3"/>
diff --git a/primefaces/src/main/java/org/primefaces/component/api/UIData.java b/primefaces/src/main/java/org/primefaces/component/api/UIData.java
index 224a616f0c..5a19ba4194 100644
--- a/primefaces/src/main/java/org/primefaces/component/api/UIData.java
+++ b/primefaces/src/main/java/org/primefaces/component/api/UIData.java
@@ -26,6 +26,7 @@ package org.primefaces.component.api;
 import java.io.IOException;
 import java.sql.ResultSet;
 import java.util.*;
+import java.util.logging.Logger;
 
 import javax.faces.FacesException;
 import javax.faces.application.Application;
@@ -61,6 +62,7 @@ import org.primefaces.util.SharedStringBuilder;
 @SuppressWarnings("unchecked")
 public class UIData extends javax.faces.component.UIData {
 
+    private static final Logger LOGGER = Logger.getLogger(UIData.class.getName());
     private static final String SB_ID = UIData.class.getName() + "#id";
 
     private final Map<String, Object> _rowTransientStates = new HashMap<>();
@@ -82,16 +84,20 @@ public class UIData extends javax.faces.component.UIData {
 
     public boolean isLazy() {
         return ComponentUtils.eval(getStateHelper(), PropertyKeys.lazy, () -> {
+            boolean lazy = false;
+
             // if not set by xhtml, we need to check the type of the value binding
             Class<?> type = ELUtils.getType(getFacesContext(),
                     getValueExpression("value"),
                     () -> getValue());
             if (type == null) {
-                throw new FacesException("Unable to automatically determine the `lazy` attribute. "
+                LOGGER.warning("Unable to automatically determine the `lazy` attribute, fallback to false. "
                         + "Either define the `lazy` attribute on the component or make sure the `value` attribute doesn't resolve to `null`. "
                         + "clientId: " + this.getClientId());
             }
-            boolean lazy = LazyDataModel.class.isAssignableFrom(type);
+            else {
+                lazy = LazyDataModel.class.isAssignableFrom(type);
+            }
 
             // remember in ViewState, to not do the same check again
             setLazy(lazy);
diff --git a/primefaces/src/main/java/org/primefaces/component/card/CardRenderer.java b/primefaces/src/main/java/org/primefaces/component/card/CardRenderer.java
index 48fba09302..755032e5dc 100644
--- a/primefaces/src/main/java/org/primefaces/component/card/CardRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/card/CardRenderer.java
@@ -111,7 +111,7 @@ public class CardRenderer extends CoreRenderer {
         writer.endElement("div");
 
         //footer
-        String footer = card.getSubtitle();
+        String footer = card.getFooter();
         if (LangUtils.isNotBlank(footer)) {
             writer.startElement("div", card);
             writer.writeAttribute("class", Card.FOOTER_CLASS, null);
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 6233c12c71..15552970a2 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
@@ -39,6 +39,7 @@ import javax.faces.event.PhaseId;
 import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.primefaces.util.ComponentUtils;
 
 public class FilterFeature implements DataTableFeature {
 
@@ -208,7 +209,7 @@ public class FilterFeature implements DataTableFeature {
         }
 
         //Metadata for callback
-        if (table.isPaginator() || table.isVirtualScroll()) {
+        if ((table.isPaginator() || table.isVirtualScroll()) && ComponentUtils.isRequestSource(table, context)) {
             PrimeFaces.current().ajax().addCallbackParam("totalRecords", filtered.size());
         }
 
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/feature/SortFeature.java b/primefaces/src/main/java/org/primefaces/component/datatable/feature/SortFeature.java
index 980a57f9b8..b02dc6d7b7 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/feature/SortFeature.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/feature/SortFeature.java
@@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import javax.el.ValueExpression;
+import org.primefaces.util.ComponentUtils;
 
 public class SortFeature implements DataTableFeature {
 
@@ -120,7 +121,7 @@ public class SortFeature implements DataTableFeature {
 
             sort(context, table);
 
-            if (table.isPaginator()) {
+            if (table.isPaginator() && ComponentUtils.isRequestSource(table, context)) {
                 PrimeFaces.current().ajax().addCallbackParam("totalRecords", table.getRowCount());
             }
 
diff --git a/primefaces/src/main/java/org/primefaces/component/dataview/DataView.java b/primefaces/src/main/java/org/primefaces/component/dataview/DataView.java
index 9c2a995365..7f9020bb67 100644
--- a/primefaces/src/main/java/org/primefaces/component/dataview/DataView.java
+++ b/primefaces/src/main/java/org/primefaces/component/dataview/DataView.java
@@ -158,7 +158,7 @@ public class DataView extends DataViewBase {
             lazyModel.setWrappedData(data);
 
             //Update paginator
-            if (ComponentUtils.isRequestSource(this, getFacesContext()) && isPaginator() ) {
+            if (ComponentUtils.isRequestSource(this, getFacesContext()) && isPaginator()) {
                 PrimeFaces.current().ajax().addCallbackParam("totalRecords", lazyModel.getRowCount());
             }
         }
diff --git a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
index df0c92cae9..31fe19933e 100644
--- a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
@@ -91,7 +91,9 @@ public class SelectOneRadioRenderer extends SelectOneRenderer {
         WidgetBuilder wb = getWidgetBuilder(context);
         wb.init("SelectOneRadio", radio)
                 .attr("custom", custom, false)
-                .attr("unselectable", radio.isUnselectable()).finish();
+                .attr("unselectable", radio.isUnselectable())
+                .attr("readonly", radio.isReadonly(), false)
+                .finish();
     }
 
     protected void encodeResponsiveLayout(FacesContext context, SelectOneRadio radio) throws IOException {
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 085483316d..19390f295a 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
@@ -44,6 +44,7 @@ import org.primefaces.component.treetable.TreeTableState;
 import org.primefaces.model.CheckboxTreeNode;
 import org.primefaces.model.DefaultTreeNode;
 import org.primefaces.model.TreeNode;
+import org.primefaces.util.ComponentUtils;
 import org.primefaces.util.LocaleUtils;
 
 public class FilterFeature implements TreeTableFeature {
@@ -136,11 +137,13 @@ public class FilterFeature implements TreeTableFeature {
         tt.setRowKey(root, null);
 
         //Metadata for callback
-        if (tt.isPaginator()) {
-            PrimeFaces.current().ajax().addCallbackParam("totalRecords", filteredValue.getChildCount());
-        }
-        if (tt.getSelectedRowKeysAsString() != null) {
-            PrimeFaces.current().ajax().addCallbackParam("selection", tt.getSelectedRowKeysAsString());
+        if (ComponentUtils.isRequestSource(tt, context)) {
+            if (tt.isPaginator()) {
+                PrimeFaces.current().ajax().addCallbackParam("totalRecords", filteredValue.getChildCount());
+            }
+            if (tt.getSelectedRowKeysAsString() != null) {
+                PrimeFaces.current().ajax().addCallbackParam("selection", tt.getSelectedRowKeysAsString());
+            }
         }
     }
 
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/feature/SelectionFeature.java b/primefaces/src/main/java/org/primefaces/component/treetable/feature/SelectionFeature.java
index 74fa5f723f..d1fed4e4f7 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/feature/SelectionFeature.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/feature/SelectionFeature.java
@@ -33,6 +33,7 @@ import org.primefaces.PrimeFaces;
 import org.primefaces.component.treetable.TreeTable;
 import org.primefaces.component.treetable.TreeTableRenderer;
 import org.primefaces.model.TreeNode;
+import org.primefaces.util.ComponentUtils;
 import org.primefaces.util.SharedStringBuilder;
 
 public class SelectionFeature implements TreeTableFeature {
@@ -109,7 +110,9 @@ public class SelectionFeature implements TreeTableFeature {
                 builder.append(descendantRowKeys.get(i));
             }
 
-            PrimeFaces.current().ajax().addCallbackParam("descendantRowKeys", builder.toString());
+            if (ComponentUtils.isRequestSource(table, context)) {
+                PrimeFaces.current().ajax().addCallbackParam("descendantRowKeys", builder.toString());
+            }
         }
     }
 
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/feature/SortFeature.java b/primefaces/src/main/java/org/primefaces/component/treetable/feature/SortFeature.java
index 4af0db92b1..570c3c5a7e 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/feature/SortFeature.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/feature/SortFeature.java
@@ -38,6 +38,7 @@ import java.util.stream.IntStream;
 import org.primefaces.component.treetable.TreeTable;
 import org.primefaces.component.treetable.TreeTableRenderer;
 import org.primefaces.component.treetable.TreeTableState;
+import org.primefaces.util.ComponentUtils;
 
 public class SortFeature implements TreeTableFeature {
 
@@ -109,11 +110,11 @@ public class SortFeature implements TreeTableFeature {
         renderer.encodeTbody(context, table, table.getValue(), true);
 
         String selectedRowKeys = table.getSelectedRowKeysAsString();
-        if (selectedRowKeys != null) {
+        if (selectedRowKeys != null && ComponentUtils.isRequestSource(table, context)) {
             PrimeFaces.current().ajax().addCallbackParam("selection", selectedRowKeys);
         }
 
-        if (table.isPaginator()) {
+        if (table.isPaginator() && ComponentUtils.isRequestSource(table, context)) {
             PrimeFaces.current().ajax().addCallbackParam("totalRecords", table.getRowCount());
         }
 
@@ -185,7 +186,7 @@ public class SortFeature implements TreeTableFeature {
             });
 
             for (int i = 0; i < childrenArray.length; i++) {
-                children.setSibling(i, (TreeNode) childrenArray[i]);
+                children.set(i, (TreeNode) childrenArray[i]);
             }
             for (int i = 0; i < children.size(); i++) {
                 sortNode(table, sortBy, comparisonResult, children.get(i), context, var, locale, collator);
diff --git a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
index 5effcbaa86..d341a9ed61 100644
--- a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
+++ b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
@@ -150,35 +150,6 @@ public class CheckboxTreeNodeChildren<T> extends TreeNodeList<T> {
         }
     }
 
-    /**
-     * Optimized set implementation to be used in sorting
-     *
-     * @param index index of the element to replace
-     * @param node node to be stored at the specified position
-     * @return the node previously at the specified position
-     */
-    @Override
-    public TreeNode setSibling(int index, TreeNode node) {
-        if (node == null) {
-            throw new NullPointerException();
-        }
-        else if ((index < 0) || (index >= size())) {
-            throw new IndexOutOfBoundsException();
-        }
-        else {
-            if (!parent.equals(node.getParent())) {
-                eraseParent(node);
-            }
-
-            TreeNode previous = get(index);
-            super.set(index, node);
-            node.setParent(parent);
-            updateRowKeys(parent);
-            updateSelectionState(parent);
-            return previous;
-        }
-    }
-
     @Override
     public TreeNode remove(int index) {
         TreeNode node = get(index);
diff --git a/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java b/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
index faa43771fa..9fa05402c8 100644
--- a/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
+++ b/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
@@ -135,34 +135,6 @@ public class TreeNodeChildren<T> extends TreeNodeList<T> {
             eraseParent(node);
         }
 
-        TreeNode previous = get(index);
-        super.set(index, node);
-        previous.setParent(null);
-        node.setParent(parent);
-        updateRowKeys(parent, node, index);
-        return previous;
-    }
-
-    /**
-     * Optimized set implementation to be used in sorting
-     *
-     * @param index index of the element to replace
-     * @param node node to be stored at the specified position
-     * @return the node previously at the specified position
-     */
-    @Override
-    public TreeNode setSibling(int index, TreeNode node) {
-        if (node == null) {
-            throw new NullPointerException();
-        }
-        if (index < 0 || index >= size()) {
-            throw new IndexOutOfBoundsException();
-        }
-
-        if (!parent.equals(node.getParent())) {
-            eraseParent(node);
-        }
-
         TreeNode previous = get(index);
         super.set(index, node);
         node.setParent(parent);
diff --git a/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java b/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
index 71425d6556..68e240cb60 100644
--- a/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
+++ b/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
@@ -29,14 +29,4 @@ public class TreeNodeList<T> extends ArrayList<TreeNode<T>> {
 
     private static final long serialVersionUID = 1L;
 
-    /**
-     * Optimized set implementation to be used in sorting
-     *
-     * @param index index of the element to replace
-     * @param node node to be stored at the specified position
-     * @return the node previously at the specified position
-     */
-    public TreeNode<T> setSibling(int index, TreeNode<T> node) {
-        throw new UnsupportedOperationException();
-    }
 }
diff --git a/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml b/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
index ba0aa2272a..672260ce32 100644
--- a/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
+++ b/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
@@ -24853,6 +24853,14 @@
             <required>false</required>
             <type>boolean</type>
         </attribute>
+        <attribute>
+            <description>
+                <![CDATA[Flag indicating that this input element will prevent changes by the user.]]>
+            </description>
+            <name>readonly</name>
+            <required>false</required>
+            <type>boolean</type>
+        </attribute>
     </tag>
     <tag>
         <description>
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/1-datepicker.js b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/1-datepicker.js
index e6a4a81145..c4af5b32e2 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/1-datepicker.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/1-datepicker.js
@@ -251,6 +251,10 @@ PrimeFaces.widget.DatePicker = PrimeFaces.widget.BaseWidget.extend({
         this.cfg.onPanelCreate = function() {
             $this.panel = this.panel;
             $this.cfg.appendTo = PrimeFaces.utils.resolveAppendTo($this, $this.panel);
+            // #8423
+            if ($this.cfg.inline) {
+                $this.panel.css('position', '');
+            }
             this.options.appendTo = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector($this.cfg.appendTo);
         };
     },
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/forms/forms.selectoneradio.js b/primefaces/src/main/resources/META-INF/resources/primefaces/forms/forms.selectoneradio.js
index e10fcaa54f..b26f50855a 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/forms/forms.selectoneradio.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/forms/forms.selectoneradio.js
@@ -21,6 +21,7 @@
  *
  * @prop {boolean} cfg.custom Whether a custom layout is enabled.
  * @prop {boolean} cfg.unselectable Unselectable mode when true clicking a radio again will clear the selection.
+ * @prop {boolean} cfg.readonly Whether the radio group is readonly.
  */
 PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
 
@@ -32,6 +33,7 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
     init: function(cfg) {
         this._super(cfg);
         var $this = this;
+        this.cfg.readonly = this.cfg.readonly === true;
 
         //custom layout
         if(this.cfg.custom) {
@@ -127,52 +129,54 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
     bindEvents: function() {
         var $this = this;
 
-        this.outputs.filter(':not(.ui-state-disabled)').on('mouseenter.selectOneRadio', function() {
-            $(this).addClass('ui-state-hover');
-        })
-        .on('mouseleave.selectOneRadio', function() {
-            $(this).removeClass('ui-state-hover');
-        })
-        .on('click.selectOneRadio', function(e) {
-            var radio = $(this),
-            input = radio.prev().children(':radio');
-
-            $this.jq.find('[role=radio]').attr('aria-checked', false);
-            $this.setAriaChecked(radio, true);
-            if(!radio.hasClass('ui-state-active')) {
-                $this.unselect($this.checkedRadio);
-                $this.select(radio);
-                $this.fireClickEvent(input, e);
-                input.trigger('change');
-            }
-            else {
-                if ($this.cfg.unselectable) {
+        if (!this.cfg.readonly) {
+            this.outputs.filter(':not(.ui-state-disabled)').on('mouseenter.selectOneRadio', function() {
+                $(this).addClass('ui-state-hover');
+            })
+            .on('mouseleave.selectOneRadio', function() {
+                $(this).removeClass('ui-state-hover');
+            })
+            .on('click.selectOneRadio', function(e) {
+                var radio = $(this),
+                input = radio.prev().children(':radio');
+
+                $this.jq.find('[role=radio]').attr('aria-checked', false);
+                $this.setAriaChecked(radio, true);
+                if(!radio.hasClass('ui-state-active')) {
                     $this.unselect($this.checkedRadio);
+                    $this.select(radio);
+                    $this.fireClickEvent(input, e);
+                    input.trigger('change');
+                }
+                else {
+                    if ($this.cfg.unselectable) {
+                        $this.unselect($this.checkedRadio);
+                    }
+                    $this.fireClickEvent(input, e);
                 }
-                $this.fireClickEvent(input, e);
-            }
 
-            input.trigger('focus.selectOneRadio');
+                input.trigger('focus.selectOneRadio');
 
-            // Github issue #4467
-            e.stopPropagation();
-            e.preventDefault();
-        });
+                // Github issue #4467
+                e.stopPropagation();
+                e.preventDefault();
+            });
 
-        this.labels.filter(':not(.ui-state-disabled)').on('click.selectOneRadio', function(e) {
-            var target = $(PrimeFaces.escapeClientId($(this).attr('for'))),
-            radio = null;
+            this.labels.filter(':not(.ui-state-disabled)').on('click.selectOneRadio', function(e) {
+                var target = $(PrimeFaces.escapeClientId($(this).attr('for'))),
+                radio = null;
 
-            //checks if target is input or not(custom labels)
-            if(target.is(':input'))
-                radio = target.parent().next();
-            else
-                radio = target.children('.ui-radiobutton-box'); //custom layout
+                //checks if target is input or not(custom labels)
+                if(target.is(':input'))
+                    radio = target.parent().next();
+                else
+                    radio = target.children('.ui-radiobutton-box'); //custom layout
 
-            radio.trigger('click.selectOneRadio');
+                radio.trigger('click.selectOneRadio');
 
-            e.preventDefault();
-        });
+                e.preventDefault();
+            });
+        }
 
         this.enabledInputs.on('focus.selectOneRadio', function() {
             var input = $(this),
@@ -187,6 +191,10 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
             radio.removeClass('ui-state-focus');
         })
         .on('keydown.selectOneRadio', function(e) {
+            if (this.cfg.readonly) {
+                return;
+            }
+
             var input = $(this),
             currentRadio = input.parent().next(),
             index = $this.enabledInputs.index(input),
@@ -240,6 +248,10 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
      * @param {JQuery} radio A radio button of this widget to unselect.
      */
     unselect: function(radio) {
+        if (this.cfg.readonly) {
+            return;
+        }
+
         var radioInput = radio.prev().children(':radio');
         radioInput.prop('checked', false);
         radio.removeClass('ui-state-active').children('.ui-radiobutton-icon').removeClass('ui-icon-bullet').addClass('ui-icon-blank');
@@ -255,6 +267,10 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
      * @param {JQuery} radio A radio button of this widget to select.
      */
     select: function(radio) {
+        if (this.cfg.readonly) {
+            return;
+        }
+
         var radioInput = radio.prev().children(':radio');
         this.checkedRadio = radio;
         radio.addClass('ui-state-active').children('.ui-radiobutton-icon').addClass('ui-icon-bullet').removeClass('ui-icon-blank');
@@ -289,6 +305,10 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
      * @param {number} index Index of the radio button option to disable.
      */
     disable: function(index) {
+        if (this.cfg.readonly) {
+            return;
+        }
+
         if(index == null) {
             this.inputs.attr('disabled', 'disabled');
             this.labels.addClass('ui-state-disabled');
@@ -302,7 +322,6 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
             label.addClass('ui-state-disabled');
             this.unbindEvents(input);
         }
-
     },
 
     /**
@@ -310,6 +329,10 @@ PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({
      * @param {number} index Index of the radio button option to enable.
      */
     enable: function(index) {
+        if (this.cfg.readonly) {
+            return;
+        }
+
         if(index == null) {
             this.inputs.removeAttr('disabled');
             this.labels.removeClass('ui-state-disabled');
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/validation/validation.validators.js b/primefaces/src/main/resources/META-INF/resources/primefaces/validation/validation.validators.js
index a63aa6c02e..b982cb9a1f 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/validation/validation.validators.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/validation/validation.validators.js
@@ -36,7 +36,7 @@ if (window.PrimeFaces) {
                 max = element.data('p-maxvalue'),
                 vc = PrimeFaces.validation.ValidationContext;
 
-                if(!this.regex.test(element.val())) {
+                if(!this.regex.test(value)) {
                     throw vc.getMessage(this.TYPE_MESSAGE_ID, vc.getLabel(element));
                 }
 
@@ -66,7 +66,7 @@ if (window.PrimeFaces) {
                 max = element.data('p-maxvalue'),
                 vc = PrimeFaces.validation.ValidationContext;
 
-                if(!this.regex.test(element.val())) {
+                if(!this.regex.test(value)) {
                     throw vc.getMessage(this.TYPE_MESSAGE_ID, vc.getLabel(element));
                 }
 


Test patch:
diff --git a/primefaces-integration-tests/pom.xml b/primefaces-integration-tests/pom.xml
index c2efe08290..5fbb7ec7ab 100644
--- a/primefaces-integration-tests/pom.xml
+++ b/primefaces-integration-tests/pom.xml
@@ -29,7 +29,7 @@
         <javax.faces.STATE_SAVING_METHOD>server</javax.faces.STATE_SAVING_METHOD>
 
         <!-- dependency versions -->
-        <hibernate-validator.version>6.2.1.Final</hibernate-validator.version>
+        <hibernate-validator.version>6.2.2.Final</hibernate-validator.version>
         <lombok.version>1.18.22</lombok.version>
         <myfaces.version>2.3.9</myfaces.version>
         <myfaces-next.version>2.3-next-M6</myfaces-next.version>
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005.java
new file mode 100644
index 0000000000..33511c835b
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005.java
@@ -0,0 +1,58 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.selectoneradio;
+
+import java.io.Serializable;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.primefaces.integrationtests.general.model.Driver;
+import org.primefaces.integrationtests.general.service.RealDriverService;
+
+import lombok.Data;
+
+@Named
+@ViewScoped
+@Data
+public class SelectOneRadio005 implements Serializable {
+
+    private static final long serialVersionUID = -7518459955779385834L;
+
+    @Inject
+    private RealDriverService driverService;
+    private List<Driver> drivers;
+
+    private int value;
+
+    @PostConstruct
+    public void init() {
+        drivers = driverService.getDrivers();
+        value = 4;
+    }
+
+}
diff --git a/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio005.xhtml b/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio005.xhtml
new file mode 100644
index 0000000000..b4f98d7519
--- /dev/null
+++ b/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio005.xhtml
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html"
+      xmlns:f="http://xmlns.jcp.org/jsf/core"
+      xmlns:p="http://primefaces.org/ui">
+
+<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
+    <h:head>
+
+    </h:head>
+
+    <h:body>
+
+        <h:form id="form">
+            <p:selectOneRadio id="selectoneradio" value="#{selectOneRadio005.value}" readonly="true">
+                <f:selectItems value="#{selectOneRadio005.drivers}" var="driver" itemLabel="#{driver.name}" itemValue="#{driver.id}"/>
+            </p:selectOneRadio>
+
+            <p:commandButton id="submit" update="@form" value="Submit"/>
+        </h:form>
+
+    </h:body>
+</f:view>
+
+</html>
\ No newline at end of file
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/chips/Chips004Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/chips/Chips004Test.java
index 85572ecfa3..f8d4ac8eb5 100644
--- a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/chips/Chips004Test.java
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/chips/Chips004Test.java
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test;
 import org.openqa.selenium.support.FindBy;
 import org.primefaces.selenium.AbstractPrimePage;
 import org.primefaces.selenium.AbstractPrimePageTest;
+import org.primefaces.selenium.PrimeSelenium;
 import org.primefaces.selenium.component.Chips;
 import org.primefaces.selenium.component.CommandButton;
 import org.primefaces.selenium.component.InputText;
@@ -46,6 +47,15 @@ public class Chips004Test extends AbstractPrimePageTest {
     @DisplayName("Chips: GitHub #1895/#6691: Chips allow pasting of delimited list")
     @Tag("SafariExclude") // can't get copy and paste working on Safari
     public void testClipboardPaste(Page page) {
+        if (PrimeSelenium.isChrome()) {
+            /*
+             * Chrome 98 - bug
+             * https://bugs.chromium.org/p/chromedriver/issues/detail?id=3999
+             * Re-enable with Chrome 99!
+             */
+            return;
+        }
+
         // Arrange
         Chips chips = page.chips;
         InputText clipboard = page.clipboard;
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005Test.java
new file mode 100644
index 0000000000..eea8f01e67
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio005Test.java
@@ -0,0 +1,67 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.selectoneradio;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.support.FindBy;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.AbstractPrimePageTest;
+import org.primefaces.selenium.component.CommandButton;
+import org.primefaces.selenium.component.SelectOneRadio;
+
+public class SelectOneRadio005Test extends AbstractPrimePageTest {
+
+    @Test
+    @Order(1)
+    @DisplayName("SelectOneRadio: Readonly")
+    public void testReadonly(Page page) {
+        // Arrange
+        SelectOneRadio selectOneRadio = page.selectOneRadio;
+        Assertions.assertTrue(selectOneRadio.isReadOnly());
+        Assertions.assertEquals("Lando", selectOneRadio.getSelectedLabel());
+
+        // Act
+        selectOneRadio.select("Charles");
+        page.submit.click();
+
+        // Assert
+        Assertions.assertEquals("Lando", selectOneRadio.getSelectedLabel());
+    }
+
+    public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:selectoneradio")
+        SelectOneRadio selectOneRadio;
+
+        @FindBy(id = "form:submit")
+        CommandButton submit;
+
+        @Override
+        public String getLocation() {
+            return "selectoneradio/selectOneRadio005.xhtml";
+        }
+    }
+}
diff --git a/primefaces/src/test/java/org/primefaces/model/TreeNodeChildrenTest.java b/primefaces/src/test/java/org/primefaces/model/TreeNodeChildrenTest.java
new file mode 100644
index 0000000000..f4f43a8cbe
--- /dev/null
+++ b/primefaces/src/test/java/org/primefaces/model/TreeNodeChildrenTest.java
@@ -0,0 +1,62 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.model;
+
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TreeNodeChildrenTest {
+
+    @Test
+    public void swapRetainsParent() {
+        // Arrange
+        TreeNode<String> root = new DefaultTreeNode<>("Parent", null);
+        TreeNode<String> child1 = new DefaultTreeNode<>("Child1", root);
+        TreeNode<String> child2 = new DefaultTreeNode<>("Child2", root);
+
+        List<TreeNode<String>> values = new TreeNodeChildren<>(root);
+
+        values.add(child1);
+        values.add(child2);
+
+        // Pre-assert
+        assertEquals(child1, values.get(0));
+        assertEquals(root, values.get(0).getParent());
+        assertEquals(child2, values.get(1));
+        assertEquals(root, values.get(1).getParent());
+
+        // Act
+        Collections.swap(values, 0, 1);
+
+        // Assert
+        assertEquals(child2, values.get(0));
+        assertEquals(root, values.get(0).getParent());
+        assertEquals(child1, values.get(1));
+        assertEquals(root, values.get(1).getParent());
+    }
+
+}