Repository: xwiki/xwiki-commons
Pull request #1262: XCOMMONS-2738: Introduce the notion of mandatory property feature
Author: login: Sereza5
Created: 2025-03-12T10:10:28Z | Merged: 2025-03-13T14:32:52Z

# Jira URL

<!-- Add the link to the corresponding JIRA issue referenced in a commit message. Unless this is a [Misc] commit,
see https://dev.xwiki.org/xwiki/bin/view/Community/DevelopmentPractices#HRule:Don27tcreateunnecessaryissues
-->
https://jira.xwiki.org/browse/XCOMMONS-2738

# Changes

## Description

<!-- Describe the main changes brought in this PR. -->

* Added the annotation
* Created an exception type especially for failed mandatory feature checks.
* Added the checks for mandatory features  to the `populateBean` function
* Added a field in the propertyGroupDescriptor to hold the info contained in the annotation.
* Added tests for this new annotation (new testbean and independant tests).

## Clarifications

<!-- Provide extra hints to make it easier to understand the PR. Those could be:
* Explanation of choices made in this PR
* Anchor towards extra resources needed to understand the context of this PR (e.g., a forum proposal).
* Links to other issues this issue depends on
-->

* By default, properties do not have a mandatory feature.
* Everything should be in the javadocs and ticket description already. Once this is merged I'll take care of updating the docs with detailed info about this annotation.

# Screenshots & Video

<!-- If this PR introduces any UI change, it's recommended to highlight it with before/after screenshots 
or even a screen recording for complex interactions. 
-->

None, backend changes only.

# Executed Tests

<!-- Especially important for regression fixes. 
Indicate how changes were tested (e.g., what maven commands were run to validate them).
-->

Successfully built `mvn clean install -f xwiki-commons-properties -Pquality`. Note that after adding the tests, I realized there was an issue with the checks when populating beans and fixed it. 

# Expected merging strategy

* Prefers squash: Yes <!-- No — Explain why. -->
* Backport on branches:
  * None. This is a new annotation for properties and should not be backported.


Diff:
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
new file mode 100644
index 0000000000..c09850b6a9
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
@@ -0,0 +1,40 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties;
+
+import org.xwiki.stability.Unstable;
+
+/**
+ * Thrown when a mandatory feature is not found in the map of values to populate.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+@Unstable
+public class PropertyFeatureMandatoryException extends PropertyMandatoryException
+{
+    /**
+     * @param property the descriptor of the property with the missing mandatory feature.
+     */
+    public PropertyFeatureMandatoryException(PropertyDescriptor property)
+    {
+        super(property.getName() + ":" + property.getGroupDescriptor().getFeature());
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
index 50cac10a03..25840d6198 100644
--- a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
+++ b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
@@ -22,6 +22,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.xwiki.stability.Unstable;
+
 /**
  * Contains a {@code List<String>} to view the group as a single object and the associated feature.
  *
@@ -33,6 +35,8 @@ public class PropertyGroupDescriptor
     private List<String> group;
 
     private String feature;
+    
+    private boolean featureMandatory;
 
     /**
      * Default constructor.
@@ -71,4 +75,24 @@ public String getFeature()
     {
         return feature;
     }
+
+    /**
+     * @since 17.2.0RC1
+     * @param mandatory indicate if the feature of this property is mandatory
+     */
+    @Unstable
+    public void setFeatureMandatory(boolean mandatory)
+    {
+        this.featureMandatory = mandatory;
+    }
+
+    /**
+     * @since 17.2.0RC1
+     * @return whether the feature upheld by this property is mandatory
+     */
+    @Unstable
+    public boolean isFeatureMandatory()
+    {
+        return this.featureMandatory;
+    }
 }
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java
index 73284456ef..206f8737ad 100644
--- a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java
+++ b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java
@@ -25,6 +25,8 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.xwiki.stability.Unstable;
+
 /**
  * Use this annotation to bind a property to a feature.
  *
@@ -40,4 +42,11 @@
      * @return the group name.
      */
     String value();
+
+    /**
+     * @since 17.2.0RC1
+     * @return whether the feature described in this annotation is mandatory.
+     */
+    @Unstable
+    boolean mandatory() default false;
 }
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanDescriptor.java b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanDescriptor.java
index f4908afac9..02b112ea8e 100644
--- a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanDescriptor.java
+++ b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanDescriptor.java
@@ -299,6 +299,7 @@ private void handlePropertyFeatureAndGroupAnnotations(DefaultPropertyDescriptor
                     + "(previous: [%s], new: [%s])", desc.getId(), group.getFeature(), parameterFeature.value()));
             }
             group.setFeature(parameterFeature.value());
+            group.setFeatureMandatory(parameterFeature.mandatory());
         }
     }
 
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
index 17622d545d..4235ca4644 100644
--- a/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
+++ b/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
@@ -22,8 +22,10 @@
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 import javax.inject.Inject;
@@ -43,6 +45,8 @@
 import org.xwiki.properties.ConverterManager;
 import org.xwiki.properties.PropertyDescriptor;
 import org.xwiki.properties.PropertyException;
+import org.xwiki.properties.PropertyFeatureMandatoryException;
+import org.xwiki.properties.PropertyGroupDescriptor;
 import org.xwiki.properties.PropertyMandatoryException;
 import org.xwiki.properties.RawProperties;
 
@@ -137,6 +141,7 @@ public void populate(Object bean, Map<String, ?> values) throws PropertyExceptio
     private void populateBean(Object bean, Map<String, Object> values) throws PropertyException
     {
         BeanDescriptor beanDescriptor = getBeanDescriptor(bean.getClass());
+        Map<String, Object> alreadyPopulatedProperties = new HashMap<>();
 
         // Lower case provided properties to easily ignore properties name case
         Map<String, String> lowerKeyMap = new HashMap<>(values.size());
@@ -178,10 +183,14 @@ private void populateBean(Object bean, Map<String, Object> values) throws Proper
                 }
 
                 // "Tick" already populated properties
+                alreadyPopulatedProperties.put(propertyId, value);
                 values.remove(propertyId);
             } else if (propertyDescriptor.isMandatory()) {
                 throw new PropertyMandatoryException(propertyId);
             }
+            Map<String, Object> allPropertyValues = new HashMap<>(values);
+            allPropertyValues.putAll(alreadyPopulatedProperties);
+            checkFeatureMandatory(propertyDescriptor, beanDescriptor.getProperties(), allPropertyValues);
         }
     }
 
@@ -219,6 +228,33 @@ private void validateBean(Object bean) throws PropertyException
         }
     }
 
+    /**
+     * We check that at least one property for this feature has a value.
+     * @param propertyDescriptor is the descriptor of the property to perform the check for
+     * @param properties is the collection of properties from which we can recover the ones that match the feature of
+     *  the current property
+     * @param values are the values associated to the properties above
+     */
+    private void checkFeatureMandatory(PropertyDescriptor propertyDescriptor,
+        Collection<PropertyDescriptor> properties,
+        Map<String, Object> values) throws PropertyFeatureMandatoryException
+    {
+        // Before all, we make sure we're in the case where this annotation actually means something.
+        PropertyGroupDescriptor groupDescriptor = propertyDescriptor.getGroupDescriptor();
+        if (groupDescriptor.getFeature() != null
+            && groupDescriptor.isFeatureMandatory())
+        {
+            String feature = groupDescriptor.getFeature();
+            boolean hasValue = properties.stream().anyMatch(
+                property -> Objects.equals(property.getGroupDescriptor().getFeature(), feature)
+                && values.get(property.getId()) != null);
+            if (!hasValue)
+            {
+                throw new PropertyFeatureMandatoryException(propertyDescriptor);
+            }
+        }
+    }
+
     @Override
     public BeanDescriptor getBeanDescriptor(Class<?> beanClass)
     {
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/internal/DefaultBeanManagerTest.java b/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/internal/DefaultBeanManagerTest.java
index 725718e639..ce43f5d9f6 100644
--- a/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/internal/DefaultBeanManagerTest.java
+++ b/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/internal/DefaultBeanManagerTest.java
@@ -35,6 +35,7 @@
 import org.xwiki.properties.internal.converter.EnumConverter;
 import org.xwiki.properties.test.GenericTestConverter;
 import org.xwiki.properties.test.TestBean;
+import org.xwiki.properties.test.TestBeanFeatures;
 import org.xwiki.properties.test.TestBeanValidation;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
@@ -150,4 +151,37 @@ void populateRawProperties() throws PropertyException
 
         assertEquals(values, bean);
     }
+    
+    @Test
+    void populateMandatoryFeatures() throws PropertyException
+    {
+        // The following tries should not throw any exception
+        // We test when a property defined before the annotated one is filled up
+        Map<String, Object> values1 = new HashMap<>();
+        values1.put("propertyFeatureMandatory1", "value");
+        this.defaultBeanManager.populate(new TestBeanFeatures(), values1);
+
+        // We test when the annotated property is the one filled up
+        Map<String, Object> values2 = new HashMap<>();
+        values2.put("propertyFeatureMandatory2", "value");
+        this.defaultBeanManager.populate(new TestBeanFeatures(), values2);
+
+        // We test when a property defined after the annotated one is filled up
+        Map<String, Object> values3 = new HashMap<>();
+        values3.put("propertyFeatureMandatory3", "value");
+        this.defaultBeanManager.populate(new TestBeanFeatures(), values3);
+
+        // We test when all properties are filled up for the feature.
+        Map<String, Object> values4 = new HashMap<>();
+        values4.put("propertyFeatureMandatory1", "value");
+        values4.put("propertyFeatureMandatory2", "value");
+        values4.put("propertyFeatureMandatory3", "value");
+        this.defaultBeanManager.populate(new TestBeanFeatures(), values4);
+
+        // We test when no property is filled up for the feature.
+        Throwable exception = assertThrows(PropertyException.class,
+            () -> this.defaultBeanManager.populate(new TestBeanFeatures(), new HashMap<>()));
+        assertEquals("Property [propertyFeatureMandatory2:mandatoryFeature] mandatory", 
+            exception.getMessage());
+    }
 }
diff --git a/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/test/TestBeanFeatures.java b/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/test/TestBeanFeatures.java
new file mode 100644
index 0000000000..7c2c1f4356
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/test/TestBeanFeatures.java
@@ -0,0 +1,64 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.test;
+
+import org.xwiki.properties.annotation.PropertyFeature;
+
+public class TestBeanFeatures
+{
+
+    private String propertyFeatureMandatory1;
+
+    private String propertyFeatureMandatory2;
+
+    private String propertyFeatureMandatory3;
+
+    @PropertyFeature("mandatoryFeature")
+    public String getPropertyFeatureMandatory1()
+    {
+        return propertyFeatureMandatory1;
+    }
+    public void setPropertyFeatureMandatory1(String propertyFeatureMandatory1)
+    {
+        this.propertyFeatureMandatory1 = propertyFeatureMandatory1;
+    }
+    
+    @PropertyFeature(value = "mandatoryFeature", mandatory = true)
+    public String getPropertyFeatureMandatory2()
+    {
+        return propertyFeatureMandatory2;
+    }
+
+    public void setPropertyFeatureMandatory2(String propertyFeatureMandatory2)
+    {
+        this.propertyFeatureMandatory2 = propertyFeatureMandatory2;
+    }
+
+    @PropertyFeature("mandatoryFeature")
+    public String getPropertyFeatureMandatory3()
+    {
+        return propertyFeatureMandatory3;
+    }
+
+    public void setPropertyFeatureMandatory3(String propertyFeatureMandatory3)
+    {
+        this.propertyFeatureMandatory3 = propertyFeatureMandatory3;
+    }
+}


Reviews:
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:29:07Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:29:56Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:30:11Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:30:32Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:34:51Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:42:24Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:48:26Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:49:14Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:49:27Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:50:31Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:56:11Z
user: login: tmortagne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:58:20Z
user: login: surli
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T11:06:36Z
user: login: surli
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T11:09:27Z
user: login: surli
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T11:10:38Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T11:12:50Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:46:46Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:47:47Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:48:55Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:49:22Z
user: login: surli
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:12:41Z
user: login: Sereza5
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T14:05:01Z
user: login: surli
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T14:24:51Z

Review comments:
diff_hunk: @@ -0,0 +1,38 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties;
+
+/**
+ * Thrown when a mandatory feature is not found in the map of values to populate.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+public class PropertyFeatureMandatoryException extends PropertyException
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
user: login: tmortagne
body: Missing `@Unstable`.

But it's not clear to me why you need a dedicated exception for this, I would assume that using directly PropertyException with the right message should be enough.
created_at: 2025-03-12T10:29:06Z
author_association: MEMBER
diff_hunk: @@ -71,4 +73,20 @@ public String getFeature()
     {
         return feature;
     }
+
+    /**
+     * @param mandatory indicate if the feature of this property is mandatory
+     */
+    public void setFeatureMandatory(boolean mandatory)
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: tmortagne
body: Missing `@Unstable` and `@since`.
created_at: 2025-03-12T10:29:56Z
author_association: MEMBER
diff_hunk: @@ -71,4 +73,20 @@ public String getFeature()
     {
         return feature;
     }
+
+    /**
+     * @param mandatory indicate if the feature of this property is mandatory
+     */
+    public void setFeatureMandatory(boolean mandatory)
+    {
+        this.featureMandatory = mandatory;
+    }
+
+    /**
+     * @return whether the feature upheld by this property is mandatory
+     */
+    public boolean isFeatureMandatory()
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: tmortagne
body: Missing `@Unstable` and `@since`.
created_at: 2025-03-12T10:30:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: tmortagne
body: Missing `@Unstable`.
created_at: 2025-03-12T10:30:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: tmortagne
body: I guess an alternative to this new annotation would be to add a `mandatory` metadata in the `@PropertyFeature` annotation (since I assume that using `@PropertyFeatureMandatory` without `@PropertyFeature` does not make much sense).
created_at: 2025-03-12T10:34:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: Sereza5
body: > since I assume that using `@PropertyFeatureMandatory` without `@PropertyFeature` does not make much sense

:+1: 
https://github.com/xwiki/xwiki-commons/blob/781c8f51aa1cfc5325bf04c240d8b3f239afc6d0/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java#L241-L243

> I guess an alternative to this new annotation would be to add a mandatory metadata in the `@PropertyFeature` annotation

I didn't really think about this alternative solution. I have no strong preference about the solution we'd pick. The metadata representation would be useful if we consider that a property can have multiple features (though I assume that for most actual use cases there'd be at least one property only for the feature for each feature...).
created_at: 2025-03-12T10:42:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: Sereza5
body: Addressed in https://github.com/xwiki/xwiki-commons/pull/1262/commits/cc70faa26793c37a1de2ddc5949c521c6eff9c14 :+1: 
created_at: 2025-03-12T10:48:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,4 +73,20 @@ public String getFeature()
     {
         return feature;
     }
+
+    /**
+     * @param mandatory indicate if the feature of this property is mandatory
+     */
+    public void setFeatureMandatory(boolean mandatory)
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: Sereza5
body: Addressed in https://github.com/xwiki/xwiki-commons/pull/1262/commits/cc70faa26793c37a1de2ddc5949c521c6eff9c14 :+1: 
created_at: 2025-03-12T10:49:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,4 +73,20 @@ public String getFeature()
     {
         return feature;
     }
+
+    /**
+     * @param mandatory indicate if the feature of this property is mandatory
+     */
+    public void setFeatureMandatory(boolean mandatory)
+    {
+        this.featureMandatory = mandatory;
+    }
+
+    /**
+     * @return whether the feature upheld by this property is mandatory
+     */
+    public boolean isFeatureMandatory()
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: Sereza5
body: Addressed in https://github.com/xwiki/xwiki-commons/pull/1262/commits/cc70faa26793c37a1de2ddc5949c521c6eff9c14 :+1: 
created_at: 2025-03-12T10:49:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties;
+
+/**
+ * Thrown when a mandatory feature is not found in the map of values to populate.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+public class PropertyFeatureMandatoryException extends PropertyException
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
user: login: Sereza5
body: It's not needed, I figured I'd just align myself with what was already done with https://github.com/xwiki/xwiki-commons/blob/5921990f7091ebbc6589e8fb7daeaf7f7538a25c/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyMandatoryException.java#L28 

___
Added the `@Unstable` annotation for now.
created_at: 2025-03-12T10:50:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties;
+
+/**
+ * Thrown when a mandatory feature is not found in the map of values to populate.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+public class PropertyFeatureMandatoryException extends PropertyException
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
user: login: tmortagne
body: Hmm, so maybe you should extend (or directly use) `@PropertyMandatoryException` instead of `PropertyException`. Since it's semantically the same thing (some mandatory property was not provided), `PropertyFeatureMandatoryException` is just a bit more precise.
created_at: 2025-03-12T10:56:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: tmortagne
body: To me the mandatory metadata solution avoid creating inconsistent situation like having a `@PropertyFeatureMandatory` without any corresponding `@PropertyFeature` and it feels a bit easier to use (i.e. to discover the fact that a feature can be mandatory).
created_at: 2025-03-12T10:58:18Z
author_association: MEMBER
diff_hunk: @@ -33,6 +35,8 @@ public class PropertyGroupDescriptor
     private List<String> group;
 
     private String feature;
+    
+    private Boolean featureMandatory = false;
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: surli
body: should be primitive type no? apparently the getter always return a `boolean` and you're never testing null case.
created_at: 2025-03-12T11:06:36Z
author_association: MEMBER
diff_hunk: @@ -219,6 +227,32 @@ private void validateBean(Object bean) throws PropertyException
         }
     }
 
+    /**
+     * We check that at least one property for this feature has a value.
+     * @param propertyDescriptor is the descriptor of the property to perform the check for
+     * @param properties is the collection of properties from which we can recover the ones that match the feature of
+     *  the current property
+     * @param values are the values associated to the properties above
+     */
+    private void checkFeatureMandatory(PropertyDescriptor propertyDescriptor,
+        Collection<PropertyDescriptor> properties,
+        Map<String, Object> values) throws PropertyFeatureMandatoryException
+    {
+        // Before all, we make sure we're in the case where this annotation actually means something.
+        if (propertyDescriptor.getGroupDescriptor().getFeature() != null
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
user: login: surli
body: just to be sure: `propertyDescriptor.getGroupDescriptor()` can never be null? 
created_at: 2025-03-12T11:09:27Z
author_association: MEMBER
diff_hunk: @@ -219,6 +227,32 @@ private void validateBean(Object bean) throws PropertyException
         }
     }
 
+    /**
+     * We check that at least one property for this feature has a value.
+     * @param propertyDescriptor is the descriptor of the property to perform the check for
+     * @param properties is the collection of properties from which we can recover the ones that match the feature of
+     *  the current property
+     * @param values are the values associated to the properties above
+     */
+    private void checkFeatureMandatory(PropertyDescriptor propertyDescriptor,
+        Collection<PropertyDescriptor> properties,
+        Map<String, Object> values) throws PropertyFeatureMandatoryException
+    {
+        // Before all, we make sure we're in the case where this annotation actually means something.
+        if (propertyDescriptor.getGroupDescriptor().getFeature() != null
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
user: login: surli
body: Btw the code would a bit more readable by using a local variable for propertyDescriptor.getGroupDescriptor()
created_at: 2025-03-12T11:10:38Z
author_association: MEMBER
diff_hunk: @@ -219,6 +227,32 @@ private void validateBean(Object bean) throws PropertyException
         }
     }
 
+    /**
+     * We check that at least one property for this feature has a value.
+     * @param propertyDescriptor is the descriptor of the property to perform the check for
+     * @param properties is the collection of properties from which we can recover the ones that match the feature of
+     *  the current property
+     * @param values are the values associated to the properties above
+     */
+    private void checkFeatureMandatory(PropertyDescriptor propertyDescriptor,
+        Collection<PropertyDescriptor> properties,
+        Map<String, Object> values) throws PropertyFeatureMandatoryException
+    {
+        // Before all, we make sure we're in the case where this annotation actually means something.
+        if (propertyDescriptor.getGroupDescriptor().getFeature() != null
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
user: login: Sereza5
body: > just to be sure: propertyDescriptor.getGroupDescriptor() can never be null?

https://github.com/xwiki/xwiki-commons/blob/9a0ee7273430477a2702eba3d7fe92ba4e529c34/xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanDescriptor.java#L287-L292

It can be empty but not null :+1: 
created_at: 2025-03-12T11:12:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties;
+
+/**
+ * Thrown when a mandatory feature is not found in the map of values to populate.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+public class PropertyFeatureMandatoryException extends PropertyException
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyFeatureMandatoryException.java
user: login: Sereza5
body: I updated it in https://github.com/xwiki/xwiki-commons/pull/1262/commits/61917d90dcd00e8f67ce153eabbd1bd416086ea9  :+1: .

IMO it's not exactly the same, since we're not talking about one property missing, but all the properties related to the feature described as mandatory in this property. When trying to make a constructor, this difference shows and I only ended up with something a bit less descriptive than what I had before.

https://github.com/xwiki/xwiki-commons/blob/61917d90dcd00e8f67ce153eabbd1bd416086ea9/xwiki-commons-core/xwiki-commons-properties/src/test/java/org/xwiki/properties/internal/DefaultBeanManagerTest.java#L184-L185

Anyways, I won't die on this hill and both would be okay.

created_at: 2025-03-12T13:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.properties.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation on a property binded to a feature {@link PropertyFeature} to tag this feature as mandatory.
+ * A mandatory feature will enforce at least one of its properties is set.
+ *
+ * @version $Id$
+ * @since 17.2.0RC1
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Inherited
+public @interface PropertyFeatureMandatory
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeatureMandatory.java
user: login: Sereza5
body: Okay, thanks for the help! I updated the annotations and their use in https://github.com/xwiki/xwiki-commons/pull/1262/commits/61917d90dcd00e8f67ce153eabbd1bd416086ea9 :+1: 
created_at: 2025-03-12T13:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +35,8 @@ public class PropertyGroupDescriptor
     private List<String> group;
 
     private String feature;
+    
+    private Boolean featureMandatory = false;
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/PropertyGroupDescriptor.java
user: login: Sereza5
body: Yup, my bad, I didn't realize my inconsistency here.
I replaced it with the primitive type here in https://github.com/xwiki/xwiki-commons/pull/1262/commits/61917d90dcd00e8f67ce153eabbd1bd416086ea9 :+1: 
created_at: 2025-03-12T13:48:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -219,6 +227,32 @@ private void validateBean(Object bean) throws PropertyException
         }
     }
 
+    /**
+     * We check that at least one property for this feature has a value.
+     * @param propertyDescriptor is the descriptor of the property to perform the check for
+     * @param properties is the collection of properties from which we can recover the ones that match the feature of
+     *  the current property
+     * @param values are the values associated to the properties above
+     */
+    private void checkFeatureMandatory(PropertyDescriptor propertyDescriptor,
+        Collection<PropertyDescriptor> properties,
+        Map<String, Object> values) throws PropertyFeatureMandatoryException
+    {
+        // Before all, we make sure we're in the case where this annotation actually means something.
+        if (propertyDescriptor.getGroupDescriptor().getFeature() != null
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/internal/DefaultBeanManager.java
user: login: Sereza5
body: > Btw the code would a bit more readable by using a local variable for propertyDescriptor.getGroupDescriptor()

Made it a bit more readable in https://github.com/xwiki/xwiki-commons/pull/1262/commits/61917d90dcd00e8f67ce153eabbd1bd416086ea9 :+1: 
created_at: 2025-03-12T13:49:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,4 +40,9 @@
      * @return the group name.
      */
     String value();
+
+    /**
+     * @return whether the feature described in this annotation is mandatory.
+     */
+    boolean mandatory() default false;
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java
user: login: surli
body: you're missing since and unstable in there too no?
created_at: 2025-03-12T17:12:41Z
author_association: MEMBER
diff_hunk: @@ -40,4 +40,9 @@
      * @return the group name.
      */
     String value();
+
+    /**
+     * @return whether the feature described in this annotation is mandatory.
+     */
+    boolean mandatory() default false;
path: xwiki-commons-core/xwiki-commons-properties/src/main/java/org/xwiki/properties/annotation/PropertyFeature.java
user: login: Sereza5
body: Added them in https://github.com/xwiki/xwiki-commons/pull/1262/commits/ff9b70264f0571445258d07101beb41ead4adf08 :+1: 

Sorry for the repeated mistake, I'll take care of checking those annotations every time I create APIs from now on.
created_at: 2025-03-13T14:05:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: Sereza5
created_at: 2025-03-12T10:48:15Z
body: Repassed the quality build after https://github.com/xwiki/xwiki-commons/pull/1262/commits/cc70faa26793c37a1de2ddc5949c521c6eff9c14 :+1: 
author_association: CONTRIBUTOR
user: login: Sereza5
created_at: 2025-03-12T10:50:48Z
body: Thanks for the review tmortagne!
author_association: CONTRIBUTOR
user: login: Sereza5
created_at: 2025-03-12T13:51:06Z
body: Repassed the quality build after https://github.com/xwiki/xwiki-commons/pull/1262/commits/61917d90dcd00e8f67ce153eabbd1bd416086ea9 :+1: 
Note that I had to update the exception message expectation to fit the new ones. 

Thank you surli for the review! :)
author_association: CONTRIBUTOR