Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XCOMMONS-2921: Add support for overwritting the configuration from the execution context #760

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<packaging>jar</packaging>
<description>Provides the infrastructure for components needing configuration data</description>
<properties>
<xwiki.jacoco.instructionRatio>0.00</xwiki.jacoco.instructionRatio>
<xwiki.jacoco.instructionRatio>0.54</xwiki.jacoco.instructionRatio>
<!-- Name to display by the Extension Manager -->
<xwiki.extension.name>Configuration API</xwiki.extension.name>
<!-- Category to display in the Extension Manager -->
Expand All @@ -44,10 +44,21 @@
<artifactId>xwiki-commons-component-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.xwiki.commons</groupId>
<artifactId>xwiki-commons-context</artifactId>
<version>${project.version}</version>
</dependency>
Comment on lines +47 to +51
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative was to put the code in xwiki-commons-context and add there the dependency on xwiki-commons-configuration-api, but @tmortagne noted that it's more probable for a module that requires the configuration API to also require the context API, than the other way.

<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.xwiki.commons</groupId>
<artifactId>xwiki-commons-tool-test-component</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Map;

import org.xwiki.component.annotation.Role;
import org.xwiki.stability.Unstable;

/**
* @version $Id$
Expand Down Expand Up @@ -96,10 +97,12 @@ default <T> T getProperty(String key, Class<T> 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
Expand All @@ -119,4 +122,21 @@ default void setProperties(Map<String, Object> 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 <T> 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> T removeProperty(String key) throws ConfigurationSaveException
{
throw new UnsupportedOperationException("Removing a property of this configuration source is not allowed");
}
}
Copy link
Member Author

@mflorea mflorea Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not happy about this one, but I don't have a better option. I'm open to suggestions.

Copy link
Member

@tmortagne tmortagne Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what we need is actually a configuration specific thing. To me, the real need is to pass a callable which is executed between an ExecutionContext set/unset and that's useful for various other use cases than just the configuration. You would then simply call the default ConfigutationSource in the callable you pass to the helper.

For example, it could simply be something like Execution#call(Map<String, Object> properties, Callable<V> callable)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that proposal does not work well with custom storage in the ExecutionContext, which is most probably the case of the ExecutionContext ConfigurationSource.

Original file line number Diff line number Diff line change
@@ -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 <V> 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> V call(String sourceHint, Map<String, Object> temporaryConfiguration, Callable<V> callable) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> getProperties();

@Override
public void setProperties(Map<String, Object> newProperties)
{
Map<String, Object> currentProperties = getProperties();
currentProperties.clear();
currentProperties.putAll(newProperties);
}

@Override
public void setProperty(String key, Object value)
{
getProperties().put(key, value);
}

@SuppressWarnings("unchecked")
@Override
public <T> T removeProperty(String key)
{
return (T) getProperties().remove(key);
}

@SuppressWarnings("unchecked")
@Override
public <T> 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> T getProperty(String key, Class<T> 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> T getProperty(String key)
{
return (T) getProperties().get(key);
}

@Override
public List<String> getKeys()
{
return new ArrayList<>(getProperties().keySet());
}

@Override
public boolean containsKey(String key)
{
return getProperties().containsKey(key);
}

@Override
public boolean isEmpty()
{
return getProperties().isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -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<ComponentManager> componentManagerProvider;

@Override
public <V> V call(String sourceHint, Map<String, Object> temporaryConfiguration, Callable<V> callable)
throws Exception
{
ConfigurationSource configurationSource = getConfigurationSource(sourceHint);
Map<String, Pair<Boolean, Object>> 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<String, Pair<Boolean, Object>> setConfiguration(ConfigurationSource configurationSource,
Map<String, Object> temporaryConfiguration) throws ConfigurationSaveException
{
Map<String, Pair<Boolean, Object>> backup = new HashMap<>();
for (Map.Entry<String, Object> 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<String, Pair<Boolean, Object>> backup) throws ConfigurationSaveException
{
for (Map.Entry<String, Pair<Boolean, Object>> 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());
}
}
}
}
Loading