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)); + } +}