diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/pom.xml b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/pom.xml
index 5be65bf6a6..ae28185ddd 100644
--- a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/pom.xml
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/pom.xml
@@ -32,7 +32,7 @@
jar
Provides the infrastructure for components needing configuration data
- 0.00
+ 0.54
Configuration API
@@ -44,10 +44,21 @@
xwiki-commons-component-api
${project.version}
+
+ org.xwiki.commons
+ xwiki-commons-context
+ ${project.version}
+
commons-beanutils
commons-beanutils
+
+
+ org.xwiki.commons
+ xwiki-commons-tool-test-component
+ ${project.version}
+ test
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/ConfigurationSource.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/ConfigurationSource.java
index f83d066e26..b03b685647 100644
--- a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/ConfigurationSource.java
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/ConfigurationSource.java
@@ -23,6 +23,7 @@
import java.util.Map;
import org.xwiki.component.annotation.Role;
+import org.xwiki.stability.Unstable;
/**
* @version $Id$
@@ -96,10 +97,12 @@ default T getProperty(String key, Class valueClass, T defaultValue)
boolean isEmpty();
/**
- * Set a property, this will replace any previously set values.
+ * Sets the value of a property, replacing any previously set values. Note that setting a property to {@code null}
+ * doesn't necessarily remove it from the configuration source. If you want to remove a property, use
+ * {@link #removeProperty(String)} instead.
*
- * @param key The key of the property to change
- * @param value The new value
+ * @param key the key of the property to change
+ * @param value the new value
* @throws ConfigurationSaveException when an error occurs during persistence
* @since 15.9
* @since 15.5.4
@@ -119,4 +122,21 @@ default void setProperties(Map properties) throws ConfigurationS
{
throw new UnsupportedOperationException("Modifying properties of this configuration source is not allowed");
}
+
+ /**
+ * Removes a property from the configuration source. Calling {@link #containsKey()} on the same key after this
+ * method is executed successfully should return {@code false}.
+ *
+ * @param key the key of the property to remove
+ * @param the property value type
+ * @return the value of the removed property or {@code null} if the property wasn't set
+ * @throws ConfigurationSaveException when an error occurs during persistence
+ * @since 16.1.0RC1
+ * @since 15.10.6
+ */
+ @Unstable
+ default T removeProperty(String key) throws ConfigurationSaveException
+ {
+ throw new UnsupportedOperationException("Removing a property of this configuration source is not allowed");
+ }
}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/TemporaryConfigurationExecutor.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/TemporaryConfigurationExecutor.java
new file mode 100644
index 0000000000..adf8083380
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/TemporaryConfigurationExecutor.java
@@ -0,0 +1,50 @@
+/*
+ * 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.configuration;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.xwiki.component.annotation.Role;
+import org.xwiki.stability.Unstable;
+
+/**
+ * Executes a {@link Callable} using a temporary configuration.
+ *
+ * @version $Id$
+ * @since 16.1.0RC1
+ * @since 15.10.6
+ */
+@Role
+@Unstable
+public interface TemporaryConfigurationExecutor
+{
+ /**
+ * Executes the passed {@link Callable} using the given temporary configuration.
+ *
+ * @param sourceHint indicates the configuration source that should receive the temporary configuration
+ * @param temporaryConfiguration the temporary configuration to use while executing the passed {@link Callable}
+ * @param callable the code to execute
+ * @param the type of value returned by the passed {@link Callable}
+ * @return the value returned by the passed {@link Callable}
+ * @throws Exception if the passed {@link Callable} throws an exception
+ */
+ V call(String sourceHint, Map temporaryConfiguration, Callable callable) throws Exception;
+}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/AbstractMemoryConfigurationSource.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/AbstractMemoryConfigurationSource.java
new file mode 100644
index 0000000000..e6ceb892ee
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/AbstractMemoryConfigurationSource.java
@@ -0,0 +1,122 @@
+/*
+ * 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.configuration.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.beanutils.ConvertUtils;
+
+/**
+ * Base class for configuration sources that store the configuration in memory.
+ *
+ * @version $Id$
+ * @since 16.1.0RC1
+ * @since 15.10.6
+ */
+public abstract class AbstractMemoryConfigurationSource extends AbstractConfigurationSource
+{
+ protected abstract Map getProperties();
+
+ @Override
+ public void setProperties(Map newProperties)
+ {
+ Map currentProperties = getProperties();
+ currentProperties.clear();
+ currentProperties.putAll(newProperties);
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ getProperties().put(key, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T removeProperty(String key)
+ {
+ return (T) getProperties().remove(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getProperty(String key, T defaultValue)
+ {
+ T result;
+
+ if (getProperties().containsKey(key)) {
+ Object value = getProperties().get(key);
+ if (value != null && defaultValue != null && !defaultValue.getClass().isInstance(value)) {
+ value = ConvertUtils.convert(value, defaultValue.getClass());
+ }
+ result = (T) value;
+ } else {
+ result = defaultValue;
+ }
+
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getProperty(String key, Class valueClass)
+ {
+ T result;
+
+ if (getProperties().containsKey(key)) {
+ Object value = getProperties().get(key);
+ if (value != null && valueClass != null && !valueClass.isInstance(value)) {
+ value = ConvertUtils.convert(value, valueClass);
+ }
+ result = (T) value;
+ } else {
+ result = getDefault(valueClass);
+ }
+
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getProperty(String key)
+ {
+ return (T) getProperties().get(key);
+ }
+
+ @Override
+ public List getKeys()
+ {
+ return new ArrayList<>(getProperties().keySet());
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ return getProperties().containsKey(key);
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return getProperties().isEmpty();
+ }
+}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutor.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutor.java
new file mode 100644
index 0000000000..948ddabe07
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutor.java
@@ -0,0 +1,99 @@
+/*
+ * 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.configuration.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.manager.ComponentLookupException;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.ConfigurationSaveException;
+import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.configuration.TemporaryConfigurationExecutor;
+
+/**
+ * Default implementation of {@link TemporaryConfigurationExecutor}.
+ *
+ * @version $Id$
+ * @since 16.1.0RC1
+ * @since 15.10.6
+ */
+@Component
+@Singleton
+public class DefaultTemporaryConfigurationExecutor implements TemporaryConfigurationExecutor
+{
+ @Inject
+ @Named("context")
+ private Provider componentManagerProvider;
+
+ @Override
+ public V call(String sourceHint, Map temporaryConfiguration, Callable callable)
+ throws Exception
+ {
+ ConfigurationSource configurationSource = getConfigurationSource(sourceHint);
+ Map> backup = setConfiguration(configurationSource, temporaryConfiguration);
+ try {
+ return callable.call();
+ } finally {
+ restoreConfiguration(configurationSource, backup);
+ }
+ }
+
+ private ConfigurationSource getConfigurationSource(String sourceHint) throws ComponentLookupException
+ {
+ ComponentManager componentManager = this.componentManagerProvider.get();
+ return componentManager.getInstance(ConfigurationSource.class, sourceHint);
+ }
+
+ private Map> setConfiguration(ConfigurationSource configurationSource,
+ Map temporaryConfiguration) throws ConfigurationSaveException
+ {
+ Map> backup = new HashMap<>();
+ for (Map.Entry entry : temporaryConfiguration.entrySet()) {
+ backup.put(entry.getKey(), new ImmutablePair<>(configurationSource.containsKey(entry.getKey()),
+ configurationSource.getProperty(entry.getKey())));
+ configurationSource.setProperty(entry.getKey(), entry.getValue());
+ }
+ return backup;
+ }
+
+ private void restoreConfiguration(ConfigurationSource configurationSource,
+ Map> backup) throws ConfigurationSaveException
+ {
+ for (Map.Entry> entry : backup.entrySet()) {
+ if (Boolean.TRUE.equals(entry.getValue().getLeft())) {
+ // The property existed before, restore its previous value.
+ configurationSource.setProperty(entry.getKey(), entry.getValue().getRight());
+ } else {
+ // The property didn't exist before, remove it.
+ configurationSource.removeProperty(entry.getKey());
+ }
+ }
+ }
+}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSource.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSource.java
new file mode 100644
index 0000000000..e5a559404c
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSource.java
@@ -0,0 +1,67 @@
+/*
+ * 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.configuration.internal;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+
+/**
+ * Configuration source that reads from the execution context.
+ *
+ * @version $Id$
+ * @since 16.1.0RC1
+ * @since 15.10.6
+ */
+@Component
+@Named("executionContext")
+@Singleton
+public class ExecutionContextConfigurationSource extends AbstractMemoryConfigurationSource
+{
+ @Inject
+ private Execution execution;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected Map getProperties()
+ {
+ ExecutionContext executionContext = this.execution.getContext();
+ if (executionContext != null) {
+ String key = this.getClass().getName();
+ if (!executionContext.hasProperty(key)) {
+ // Initialize with an empty map that ca be modified.
+ executionContext.newProperty(key).inherited().initial(new LinkedHashMap<>()).makeFinal().nonNull()
+ .declare();
+ }
+ return (Map) executionContext.getProperty(key);
+ } else {
+ // Return an empty map that can't be modified.
+ return Collections.emptyMap();
+ }
+ }
+}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/MemoryConfigurationSource.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/MemoryConfigurationSource.java
index 58882aa56f..13785ff7e4 100644
--- a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/MemoryConfigurationSource.java
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/java/org/xwiki/configuration/internal/MemoryConfigurationSource.java
@@ -19,15 +19,12 @@
*/
package org.xwiki.configuration.internal;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Named;
import javax.inject.Singleton;
-import org.apache.commons.beanutils.ConvertUtils;
import org.xwiki.component.annotation.Component;
/**
@@ -41,7 +38,7 @@
@Component
@Singleton
@Named("memory")
-public class MemoryConfigurationSource extends AbstractConfigurationSource
+public class MemoryConfigurationSource extends AbstractMemoryConfigurationSource
{
/**
* The properties.
@@ -49,84 +46,8 @@ public class MemoryConfigurationSource extends AbstractConfigurationSource
private Map properties = new ConcurrentHashMap<>();
@Override
- public void setProperty(String key, Object value)
+ protected Map getProperties()
{
- this.properties.put(key, value);
- }
-
- @Override
- public void setProperties(Map properties)
- {
- this.properties = new ConcurrentHashMap<>(properties);
- }
-
- /**
- * @param key the key associated to the property to remove
- */
- public void removeProperty(String key)
- {
- this.properties.remove(key);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public T getProperty(String key, T defaultValue)
- {
- T result;
-
- if (this.properties.containsKey(key)) {
- Object value = this.properties.get(key);
- if (value != null && defaultValue != null && !defaultValue.getClass().isInstance(value)) {
- value = ConvertUtils.convert(value, defaultValue.getClass());
- }
- result = (T) value;
- } else {
- result = defaultValue;
- }
-
- return result;
- }
-
- @Override
- public T getProperty(String key, Class valueClass)
- {
- T result;
-
- if (this.properties.containsKey(key)) {
- Object value = this.properties.get(key);
- if (value != null && valueClass != null && !valueClass.isInstance(value)) {
- value = ConvertUtils.convert(value, valueClass);
- }
- result = (T) value;
- } else {
- result = getDefault(valueClass);
- }
-
- return result;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public T getProperty(String key)
- {
- return (T) this.properties.get(key);
- }
-
- @Override
- public List getKeys()
- {
- return new ArrayList<>(this.properties.keySet());
- }
-
- @Override
- public boolean containsKey(String key)
- {
- return this.properties.containsKey(key);
- }
-
- @Override
- public boolean isEmpty()
- {
- return this.properties.isEmpty();
+ return this.properties;
}
}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/resources/META-INF/components.txt b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/resources/META-INF/components.txt
index 7d1023be0a..d1071400f2 100644
--- a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/resources/META-INF/components.txt
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/main/resources/META-INF/components.txt
@@ -1,4 +1,6 @@
org.xwiki.configuration.internal.DefaultConfigurationSourceProvider
-org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider
+org.xwiki.configuration.internal.DefaultTemporaryConfigurationExecutor
+org.xwiki.configuration.internal.ExecutionContextConfigurationSource
org.xwiki.configuration.internal.MemoryConfigurationSource
+org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider
org.xwiki.configuration.internal.VoidConfigurationSource
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutorTest.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutorTest.java
new file mode 100644
index 0000000000..dd933e71b3
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/DefaultTemporaryConfigurationExecutorTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.configuration.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+
+import javax.inject.Named;
+import javax.inject.Provider;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectComponentManager;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.test.mockito.MockitoComponentManager;
+
+/**
+ * Unit tests for {@link DefaultTemporaryConfigurationExecutor}.
+ *
+ * @version $Id$
+ */
+@ComponentTest
+class DefaultTemporaryConfigurationExecutorTest
+{
+ @InjectMockComponents
+ private DefaultTemporaryConfigurationExecutor executor;
+
+ @InjectComponentManager
+ private MockitoComponentManager componentManager;
+
+ @MockComponent
+ @Named("context")
+ private Provider componentManagerProvider;
+
+ @BeforeEach
+ void configure()
+ {
+ when(this.componentManagerProvider.get()).thenReturn(this.componentManager);
+ }
+
+ @Test
+ void call() throws Exception
+ {
+ ConfigurationSource source = this.componentManager.registerMockComponent(ConfigurationSource.class, "foo");
+ when(source.containsKey("age")).thenReturn(true);
+ when(source.getProperty("age")).thenReturn(27);
+
+ assertEquals("done", this.executor.call("foo", Map.of("color", "blue", "age", 13), () -> {
+ return "done";
+ }));
+
+ // Set.
+ verify(source).setProperty("color", "blue");
+ verify(source).setProperty("age", 13);
+
+ // Restore.
+ verify(source).removeProperty("color");
+ verify(source).setProperty("age", 27);
+ }
+}
diff --git a/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSourceTest.java b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSourceTest.java
new file mode 100644
index 0000000000..0e988dae49
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-configuration/xwiki-commons-configuration-api/src/test/java/org/xwiki/configuration/internal/ExecutionContextConfigurationSourceTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.configuration.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.jupiter.api.Test;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+
+/**
+ * Unit tests for {@link ExecutionContextConfigurationSource}.
+ */
+@ComponentTest
+class ExecutionContextConfigurationSourceTest
+{
+ @InjectMockComponents
+ private ExecutionContextConfigurationSource source;
+
+ @MockComponent
+ private Execution execution;
+
+ private ExecutionContext executionContext = new ExecutionContext();
+
+ @Test
+ void withoutExecutionContext()
+ {
+ // Read shouldn't throw an exception.
+ assertTrue(this.source.isEmpty());
+ assertTrue(this.source.getKeys().isEmpty());
+ assertFalse(this.source.containsKey("foo"));
+ assertTrue(this.source.getProperties().isEmpty());
+ assertNull(this.source.getProperty("foo"));
+ assertEquals(13, this.source.getProperty("foo", 13));
+ assertNull(this.source.getProperty("foo", Integer.class));
+ assertEquals(Collections.emptyList(), this.source.getProperty("foo", List.class));
+ assertEquals(new Properties(), this.source.getProperty("foo", Properties.class));
+ assertEquals(13, this.source.getProperty("foo", Integer.class, 13));
+ assertNull(this.source.removeProperty("foo"));
+
+ // Write should throw an exception.
+ try {
+ this.source.setProperty("foo", "bar");
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+
+ try {
+ this.source.setProperties(Map.of("foo", "bar"));
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ @Test
+ void withExecutionContext() throws Exception
+ {
+ when(this.execution.getContext()).thenReturn(this.executionContext);
+
+ assertTrue(this.source.isEmpty());
+ this.source.setProperty("foo", "bar");
+ this.source.setProperty("age", 13);
+ assertFalse(this.source.isEmpty());
+ assertTrue(this.source.containsKey("foo"));
+ assertTrue(this.source.containsKey("age"));
+ assertFalse(this.source.containsKey("count"));
+ assertEquals("bar", this.source.getProperty("foo"));
+ assertEquals(13, this.source.getProperty("age", 27));
+ assertEquals("13", this.source.getProperty("age", "27"));
+ assertEquals("13", this.source.getProperty("age", String.class));
+ assertEquals(27, this.source.getProperty("count", Integer.class, 27));
+ assertEquals(Arrays.asList("foo", "age"), this.source.getKeys());
+ assertEquals(2, this.source.getProperties().size());
+
+ assertEquals("bar", this.source.removeProperty("foo"));
+ assertNull(this.source.removeProperty("foo"));
+ assertFalse(this.source.containsKey("foo"));
+ assertEquals(1, this.source.getProperties().size());
+
+ this.source.setProperty("foo", false);
+ assertEquals(false, this.source.getProperty("foo"));
+
+ this.source.setProperties(Map.of("color", "blue", "status", 3));
+ assertEquals("blue", this.source.getProperty("color"));
+ assertEquals(3, this.source.getProperty("status", 5));
+ assertEquals(2, this.source.getProperties().size());
+
+ assertEquals("blue", this.source.removeProperty("color"));
+
+ ExecutionContext clonedContext = new ExecutionContext();
+ clonedContext.inheritFrom(this.executionContext);
+ when(this.execution.getContext()).thenReturn(clonedContext);
+
+ assertEquals(1, this.source.getProperties().size());
+ assertEquals(3, this.source.getProperty("status", 5));
+ }
+}