mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
Add support for env var resolution in Thing configuration (#5568)
* [config] ConfigUtil: Implement methods to resolve env variable syntax to environment variable values in Configuration (values) Signed-off-by: Florian Hotze <dev@florianhotze.com> * [things] BaseThingHandler: Automatically resolve variables in configuration Signed-off-by: Florian Hotze <dev@florianhotze.com> * [config] ConfigUtil: Make envProvider protected to subclasses in tests can access it from outside the package Signed-off-by: Florian Hotze <dev@florianhotze.com> * [config] ConfigUtilTest: Add a resource lock on ConfigUtil.class Signed-off-by: Florian Hotze <dev@florianhotze.com> * [thing] BaseThingHandler: Add unit tests for config variable resolving Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * ConfigUtil: Throw IEA when a variable fails to resolve Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * Address Copilot review Signed-off-by: Florian Hotze <dev@florianhotze.com> * ConfigUtil: Add method to resolve variables & normalize config afterward Signed-off-by: Florian Hotze <dev@florianhotze.com> --------- Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
+146
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -48,11 +49,28 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Thomas Höfer - Minor changes for type normalization based on config description
|
||||
* @author Florian Hotze - Add support for environment variable substitution in config values
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigUtil {
|
||||
|
||||
private static final Pattern DEFAULT_LIST_SPLITTER = Pattern.compile("(?<!\\\\),");
|
||||
private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\{ENV:([^}]+)}");
|
||||
|
||||
private static EnvProvider envProvider = System::getenv;
|
||||
|
||||
/**
|
||||
* Setter for envProvider to allow overwriting it in tests.
|
||||
*
|
||||
* <p>
|
||||
* This <strong>MUST NOT</strong> be called in production environments as it can break environment variable
|
||||
* resolving.
|
||||
*
|
||||
* @param provider the env provider to use for resolving environment variables
|
||||
*/
|
||||
protected static void setEnvProvider(EnvProvider provider) {
|
||||
envProvider = Objects.requireNonNull(provider, "provider must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the provided (default) value of the given {@link ConfigDescriptionParameter} to the corresponding Java type.
|
||||
@@ -286,4 +304,132 @@ public class ConfigUtil {
|
||||
return Constants.OBJECTCLASS.equals(name) || ComponentConstants.COMPONENT_NAME.equals(name)
|
||||
|| ComponentConstants.COMPONENT_ID.equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a string value for the variable patterns and resolves referenced variables.
|
||||
*
|
||||
* <p>
|
||||
* Note: At the moment, only environment variables are supported.
|
||||
* If no variable is referenced, the string value is returned as-is.
|
||||
* If a referenced variable fails to resolve, a {@link IllegalArgumentException} is thrown.
|
||||
*
|
||||
* @param value the value to resolve
|
||||
* @return the resolved value
|
||||
* @throws IllegalArgumentException if a variable fails to resolve
|
||||
*/
|
||||
private static String resolveVariables(String value) throws IllegalArgumentException {
|
||||
final Matcher matcher = ENV_PATTERN.matcher(value);
|
||||
|
||||
return matcher.replaceAll(matchResult -> {
|
||||
final String envVarName = matchResult.group(1);
|
||||
final @Nullable String envVarValue = envProvider.get(envVarName);
|
||||
|
||||
if (envVarValue == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not resolve environment variable '%s'!".formatted(envVarName));
|
||||
}
|
||||
|
||||
// Safely escape the replacement string so '$' and '\' are treated as literals
|
||||
return Matcher.quoteReplacement(envVarValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves variables in the given value by replacing the variable patterns through the variable values.
|
||||
*
|
||||
* <p>
|
||||
* The following rules are applied:
|
||||
* <ol>
|
||||
* <li>If the given value is a string, it is checked for variable patterns and referenced variables are
|
||||
* resolved.</li>
|
||||
* <li>If a variable fails to resolve, a {@link IllegalArgumentException} is thrown.</li>
|
||||
* <li>If the value is a collection, this method is called for each element.</li>
|
||||
* <li>If the value is neither a string nor a collection, it is returned as-is.</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param value the value to resolve
|
||||
* @return the resolved value
|
||||
* @throws IllegalArgumentException if a variable fails to resolve
|
||||
*/
|
||||
public static Object resolveVariables(Object value) throws IllegalArgumentException {
|
||||
if (value instanceof String stringValue) {
|
||||
return resolveVariables(stringValue);
|
||||
} else if (value instanceof Collection<?> collectionValue) {
|
||||
final List<Object> entry = new ArrayList<>(collectionValue.size());
|
||||
for (final Object it : collectionValue) {
|
||||
final Object resolved = resolveVariables(it);
|
||||
entry.add(resolved);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve variables in the given configuration.
|
||||
*
|
||||
* <p>
|
||||
* Note that when substituting variables in non-TEXT values such as BOOLEAN, DECIMAL, etc., the config needs to be
|
||||
* normalized.
|
||||
*
|
||||
* @param configuration the configuration to resolve variables in
|
||||
* @return the resolved configuration
|
||||
* @throws IllegalArgumentException if a variable fails to resolve
|
||||
*/
|
||||
public static Map<String, @Nullable Object> resolveVariables(Map<String, @Nullable Object> configuration)
|
||||
throws IllegalArgumentException {
|
||||
final Map<String, @Nullable Object> resolvedProperties = new HashMap<>();
|
||||
for (final Entry<String, @Nullable Object> entry : configuration.entrySet()) {
|
||||
final @Nullable Object value = entry.getValue();
|
||||
if (value != null) {
|
||||
final Object resolved = resolveVariables(value);
|
||||
resolvedProperties.put(entry.getKey(), resolved);
|
||||
} else {
|
||||
resolvedProperties.put(entry.getKey(), null);
|
||||
}
|
||||
}
|
||||
return resolvedProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve variables in the given {@link Configuration}.
|
||||
*
|
||||
* <p>
|
||||
* Note that when substituting variables in non-TEXT values such as BOOLEAN, DECIMAL, etc., the config needs to be
|
||||
* normalized.
|
||||
*
|
||||
* @param configuration the configuration to resolve variables in
|
||||
* @return the resolved configuration
|
||||
* @throws IllegalArgumentException if a variable fails to resolve
|
||||
*/
|
||||
public static Configuration resolveVariables(Configuration configuration) throws IllegalArgumentException {
|
||||
return new Configuration(resolveVariables(configuration.getProperties()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve variables and normalize the results in the given {@link Configuration}.
|
||||
*
|
||||
* <p>
|
||||
* Normalizing after the resolve step allows to use variable substitution for non-TEXT values such as BOOLEAN,
|
||||
* DECIMAL, etc.
|
||||
*
|
||||
* @param configuration the configuration to resolve variables in and normalize afterward
|
||||
* @param configDescriptions the configuration descriptions that should be applied (must not be empty).
|
||||
* @return the normalized configuration
|
||||
* @throws IllegalArgumentException if a variable fails to refresh or the given config description is null
|
||||
*/
|
||||
public static Configuration resolveVariablesAndNormalizeTypes(Configuration configuration,
|
||||
List<ConfigDescription> configDescriptions) throws IllegalArgumentException {
|
||||
final Map<String, @Nullable Object> resolvedConfiguration = resolveVariables(configuration.getProperties());
|
||||
return new Configuration(normalizeTypes(resolvedConfiguration, configDescriptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider for environment variables.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
protected interface EnvProvider {
|
||||
@Nullable
|
||||
String get(String name);
|
||||
}
|
||||
}
|
||||
|
||||
+120
-1
@@ -14,6 +14,11 @@ package org.openhab.core.config.core;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.openhab.core.config.core.ConfigDescriptionParameter.Type.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -22,21 +27,46 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.junit.jupiter.api.parallel.ResourceAccessMode;
|
||||
import org.junit.jupiter.api.parallel.ResourceLock;
|
||||
|
||||
/**
|
||||
* @author Simon Kaufmann - Initial contribution
|
||||
* @author Florian Hotze - Add tests for env variable substitution
|
||||
*/
|
||||
@Execution(ExecutionMode.SAME_THREAD) // Force sequential test execution to avoid multi-threaded access to
|
||||
// ConfigUtil::setEnvProvider
|
||||
@ResourceLock(value = "org.openhab.core.config.core.ConfigUtil", mode = ResourceAccessMode.READ_WRITE)
|
||||
@NonNullByDefault
|
||||
public class ConfigUtilTest {
|
||||
|
||||
private final URI configUri = URI.create("system:ephemeris");
|
||||
private final ConfigUtil.EnvProvider mockEnv = mock(ConfigUtil.EnvProvider.class);
|
||||
|
||||
private final ConfigDescriptionParameterBuilder configDescriptionParameterBuilder1 = ConfigDescriptionParameterBuilder
|
||||
.create("p1", DECIMAL).withMultiple(true).withMultipleLimit(7);
|
||||
private final ConfigDescriptionParameterBuilder configDescriptionParameterBuilder2 = ConfigDescriptionParameterBuilder
|
||||
.create("p2", TEXT).withMultiple(true).withMultipleLimit(2);
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
reset(mockEnv);
|
||||
when(mockEnv.get("HOSTNAME")).thenReturn("openhab-host");
|
||||
when(mockEnv.get("PATH")).thenReturn("openhab-path");
|
||||
ConfigUtil.setEnvProvider(mockEnv);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
ConfigUtil.setEnvProvider(System::getenv);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyNormalizeDefaultTypeForTextReturnsString() {
|
||||
assertThat(ConfigUtil.getDefaultValueAsCorrectType(
|
||||
@@ -173,7 +203,7 @@ public class ConfigUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstDesciptionWinsForNormalization() {
|
||||
public void firstDescriptionWinsForNormalization() {
|
||||
ConfigDescription configDescriptionInteger = ConfigDescriptionBuilder.create(URI.create("thing:fooThing"))
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("foo", INTEGER).build()).build();
|
||||
|
||||
@@ -191,4 +221,93 @@ public class ConfigUtilTest {
|
||||
.normalizeTypes(Map.of("foo", "1"), List.of(configDescriptionString, configDescriptionInteger))
|
||||
.get("foo"), is(instanceOf(String.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesResolvesSingleEnvVariable() {
|
||||
String hostname = mockEnv.get("HOSTNAME");
|
||||
|
||||
assertEquals(hostname, ConfigUtil.resolveVariables("${ENV:HOSTNAME}"));
|
||||
assertEquals("prefix-" + hostname + "-suffix", ConfigUtil.resolveVariables("prefix-${ENV:HOSTNAME}-suffix"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesResolvesMultipleEnvVariables() {
|
||||
String hostname = mockEnv.get("HOSTNAME");
|
||||
String path = mockEnv.get("PATH");
|
||||
|
||||
assertEquals(hostname + ":" + path, ConfigUtil.resolveVariables("${ENV:HOSTNAME}:${ENV:PATH}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesThrowsIAEForUnknownEnvVariable() {
|
||||
assertThrows(IllegalArgumentException.class, () -> ConfigUtil.resolveVariables("${ENV:UNKNOWN_VAR}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesResolvesList() {
|
||||
String hostname = mockEnv.get("HOSTNAME");
|
||||
List<Object> input = List.of("plain", "${ENV:HOSTNAME}", true, 42, 3.14159);
|
||||
List<Object> expected = List.of("plain", hostname, true, 42, 3.14159);
|
||||
|
||||
assertEquals(expected, ConfigUtil.resolveVariables(input));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesPassesThroughPrimitivesNotString() {
|
||||
assertEquals(42, ConfigUtil.resolveVariables(42));
|
||||
assertEquals(3.14159, ConfigUtil.resolveVariables(3.14159));
|
||||
assertEquals(true, ConfigUtil.resolveVariables(true));
|
||||
assertEquals(false, ConfigUtil.resolveVariables(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesPassesThroughIncorrectPatterns() {
|
||||
String noEnv = "${HOSTNAME}";
|
||||
String noBraces = "$ENV:HOSTNAME";
|
||||
String unclosedBraces = "${ENV:HOSTNAME";
|
||||
|
||||
assertEquals(noEnv, ConfigUtil.resolveVariables(noEnv));
|
||||
assertEquals(noBraces, ConfigUtil.resolveVariables(noBraces));
|
||||
assertEquals(unclosedBraces, ConfigUtil.resolveVariables(unclosedBraces));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesResolvesEnvVariablesInConfiguration() {
|
||||
String hostname = mockEnv.get("HOSTNAME");
|
||||
Map<String, @Nullable Object> config = Map.of("p1", "plain", "p2", "${ENV:HOSTNAME}", "p3", true, "p4", 42,
|
||||
"p5", 3.14159);
|
||||
|
||||
Map<String, @Nullable Object> resolvedConfig = ConfigUtil.resolveVariables(config);
|
||||
assertEquals("plain", resolvedConfig.get("p1"));
|
||||
assertEquals(hostname, resolvedConfig.get("p2"));
|
||||
assertEquals(true, resolvedConfig.get("p3"));
|
||||
assertEquals(42, resolvedConfig.get("p4"));
|
||||
assertEquals(3.14159, resolvedConfig.get("p5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariablesAndNormalizeTypesResolvesThenNormalizedConfiguration() {
|
||||
String hostname = mockEnv.get("HOSTNAME");
|
||||
when(mockEnv.get("BOOLEAN")).thenReturn("true");
|
||||
when(mockEnv.get("INTEGER")).thenReturn("42");
|
||||
when(mockEnv.get("DECIMAL")).thenReturn("3.14159");
|
||||
|
||||
Map<String, @Nullable Object> config = Map.of("p1", "plain", "p2", "${ENV:HOSTNAME}", "p3", "${ENV:BOOLEAN}",
|
||||
"p4", "${ENV:INTEGER}", "p5", "${ENV:DECIMAL}");
|
||||
ConfigDescription configDescription = ConfigDescriptionBuilder.create(URI.create("thingType:fooThing"))
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("p1", TEXT).build())
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("p2", TEXT).build())
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("p3", BOOLEAN).build())
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("p4", INTEGER).build())
|
||||
.withParameter(ConfigDescriptionParameterBuilder.create("p5", DECIMAL).build()).build();
|
||||
|
||||
Configuration normalizedAndResolvedConfig = ConfigUtil
|
||||
.resolveVariablesAndNormalizeTypes(new Configuration(config), List.of(configDescription));
|
||||
|
||||
assertEquals("plain", normalizedAndResolvedConfig.get("p1"));
|
||||
assertEquals(hostname, normalizedAndResolvedConfig.get("p2"));
|
||||
assertEquals(true, normalizedAndResolvedConfig.get("p3"));
|
||||
assertEquals(new BigDecimal("42"), normalizedAndResolvedConfig.get("p4"));
|
||||
assertEquals(new BigDecimal("3.14159"), normalizedAndResolvedConfig.get("p5"));
|
||||
}
|
||||
}
|
||||
|
||||
+75
-9
@@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.config.core.ConfigDescription;
|
||||
import org.openhab.core.config.core.ConfigUtil;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.config.core.validation.ConfigValidationException;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
@@ -75,6 +76,8 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
|
||||
protected Thing thing;
|
||||
|
||||
private volatile @Nullable Configuration resolvedConfig;
|
||||
|
||||
private @Nullable ThingHandlerCallback callback;
|
||||
|
||||
/**
|
||||
@@ -132,7 +135,7 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
* @return true if the parameters would result in a modified configuration, false otherwise
|
||||
*/
|
||||
protected boolean isModifyingCurrentConfig(Map<String, Object> configurationParameters) {
|
||||
Configuration currentConfig = getConfig();
|
||||
Configuration currentConfig = getRawConfig();
|
||||
for (Entry<String, Object> entry : configurationParameters.entrySet()) {
|
||||
if (!Objects.equals(currentConfig.get(entry.getKey()), entry.getValue())) {
|
||||
return true;
|
||||
@@ -154,7 +157,18 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
@Override
|
||||
public void thingUpdated(Thing thing) {
|
||||
dispose();
|
||||
this.thing = thing;
|
||||
Configuration resolvedConfiguration;
|
||||
try {
|
||||
resolvedConfiguration = ConfigUtil.resolveVariables(thing.getConfiguration());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Updated thing '{}' with a thing containing invalid configuration '{}': {}", thing.getUID(),
|
||||
thing.getConfiguration(), e.getMessage());
|
||||
resolvedConfiguration = thing.getConfiguration();
|
||||
}
|
||||
synchronized (this) {
|
||||
this.thing = thing;
|
||||
this.resolvedConfig = resolvedConfiguration;
|
||||
}
|
||||
initialize();
|
||||
}
|
||||
|
||||
@@ -238,13 +252,43 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw configuration of the thing.
|
||||
* Variable patterns are not resolved.
|
||||
*
|
||||
* @return raw configuration of the thing
|
||||
*/
|
||||
private Configuration getRawConfig() {
|
||||
return getThing().getConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration of the thing.
|
||||
* Variable patterns are automatically resolved to the variable value.
|
||||
*
|
||||
* <p>
|
||||
* To modify the configuration, retrieve an editable configuration through {@link #editConfiguration()} and persist
|
||||
* the changed configuration with {@link #updateConfiguration(Configuration)}.
|
||||
*
|
||||
* @return configuration of the thing
|
||||
*/
|
||||
protected Configuration getConfig() {
|
||||
return getThing().getConfiguration();
|
||||
Configuration resolvedConfig = this.resolvedConfig;
|
||||
if (resolvedConfig == null) {
|
||||
synchronized (this) {
|
||||
resolvedConfig = this.resolvedConfig;
|
||||
if (resolvedConfig == null) {
|
||||
try {
|
||||
this.resolvedConfig = resolvedConfig = ConfigUtil.resolveVariables(getRawConfig());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Failed to resolve variables for configuration '{}' of thing '{}': {}",
|
||||
getRawConfig(), getThing().getUID(), e.getMessage());
|
||||
return new Configuration(getRawConfig());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Configuration(resolvedConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -476,8 +520,16 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
this.getClass().getSimpleName(), thing.getUID());
|
||||
return;
|
||||
}
|
||||
Configuration resolvedConfiguration;
|
||||
try {
|
||||
callback.validateConfigurationParameters(thing, thing.getConfiguration().getProperties());
|
||||
resolvedConfiguration = ConfigUtil.resolveVariables(thing.getConfiguration());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Attempt to update thing '{}' with a thing containing invalid configuration '{}' blocked: {}",
|
||||
thing.getUID(), thing.getConfiguration(), e.getMessage());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callback.validateConfigurationParameters(thing, resolvedConfiguration.getProperties());
|
||||
thing.getChannels().forEach(channel -> callback.validateConfigurationParameters(channel,
|
||||
channel.getConfiguration().getProperties()));
|
||||
} catch (ConfigValidationException e) {
|
||||
@@ -487,6 +539,7 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
this.resolvedConfig = resolvedConfiguration;
|
||||
this.thing = thing;
|
||||
callback.thingUpdated(thing);
|
||||
}
|
||||
@@ -499,7 +552,7 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
* @return copy of the thing configuration (not null)
|
||||
*/
|
||||
protected Configuration editConfiguration() {
|
||||
Map<String, Object> properties = this.thing.getConfiguration().getProperties();
|
||||
Map<String, Object> properties = getRawConfig().getProperties();
|
||||
return new Configuration(new HashMap<>(properties));
|
||||
}
|
||||
|
||||
@@ -512,15 +565,24 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
* @param configuration configuration, that was updated and should be persisted
|
||||
*/
|
||||
protected void updateConfiguration(Configuration configuration) {
|
||||
Map<String, Object> old = this.thing.getConfiguration().getProperties();
|
||||
Map<String, Object> old = getRawConfig().getProperties();
|
||||
Configuration oldResolved = getConfig();
|
||||
ThingHandlerCallback callback = this.callback;
|
||||
if (callback == null) {
|
||||
logger.warn("Handler {} tried updating its configuration although the handler was already disposed.",
|
||||
this.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
Configuration resolvedConfiguration;
|
||||
try {
|
||||
callback.validateConfigurationParameters(this.thing, configuration.getProperties());
|
||||
resolvedConfiguration = ConfigUtil.resolveVariables(configuration);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Attempt to apply configuration '{}' on thing '{}' blocked: {}", configuration, thing.getUID(),
|
||||
e.getMessage());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callback.validateConfigurationParameters(this.thing, resolvedConfiguration.getProperties());
|
||||
} catch (ConfigValidationException e) {
|
||||
logger.warn(
|
||||
"Attempt to apply invalid configuration '{}' on thing '{}' blocked. This is most likely a bug: {}",
|
||||
@@ -528,15 +590,19 @@ public abstract class BaseThingHandler implements ThingHandler {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.thing.getConfiguration().setProperties(configuration.getProperties());
|
||||
synchronized (this) {
|
||||
this.resolvedConfig = resolvedConfiguration;
|
||||
this.thing.getConfiguration().setProperties(configuration.getProperties());
|
||||
callback.thingUpdated(thing);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn(
|
||||
"Error while applying configuration changes: '{}: {}' - reverting configuration changes on thing '{}'.",
|
||||
e.getClass().getSimpleName(), e.getMessage(), this.thing.getUID().getAsString());
|
||||
this.thing.getConfiguration().setProperties(old);
|
||||
synchronized (this) {
|
||||
this.thing.getConfiguration().setProperties(old);
|
||||
this.resolvedConfig = oldResolved;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.thing.binding;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.junit.jupiter.api.parallel.ResourceAccessMode;
|
||||
import org.junit.jupiter.api.parallel.ResourceLock;
|
||||
import org.openhab.core.config.core.ConfigUtil;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Tests for {@link BaseThingHandler}.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@Execution(ExecutionMode.SAME_THREAD)
|
||||
@ResourceLock(value = "org.openhab.core.config.core.ConfigUtil", mode = ResourceAccessMode.READ_WRITE)
|
||||
@NonNullByDefault
|
||||
class BaseThingHandlerTest {
|
||||
|
||||
private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("test:type");
|
||||
private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thing");
|
||||
|
||||
private @NonNullByDefault({}) ThingHandlerCallback callback;
|
||||
private @NonNullByDefault({}) TestThingHandler handler;
|
||||
|
||||
private static class TestThingHandler extends BaseThingHandler {
|
||||
public @Nullable Configuration configInInitialize = null;
|
||||
|
||||
public TestThingHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
this.configInInitialize = getConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigUtilAccessor extends ConfigUtil {
|
||||
static void setEnv(Map<String, String> values) {
|
||||
setEnvProvider(values::get);
|
||||
}
|
||||
|
||||
static void resetEnv() {
|
||||
setEnvProvider(System::getenv);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
callback = mock(ThingHandlerCallback.class);
|
||||
Thing thing = ThingBuilder.create(THING_TYPE_UID, THING_UID.getId())
|
||||
.withConfiguration(new Configuration(Map.of("p1", "${ENV:VAR}", "p2", "${ENV:FOO}"))).build();
|
||||
handler = new TestThingHandler(thing);
|
||||
handler.setCallback(callback);
|
||||
|
||||
ConfigUtilAccessor.setEnv(Map.of("VAR", "resolved-var", "FOO", "resolved-foo", "BAR", "resolved-bar"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception {
|
||||
ConfigUtilAccessor.resetEnv();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfig() {
|
||||
Configuration config = handler.getConfig();
|
||||
assertEquals("resolved-var", config.get("p1"));
|
||||
assertEquals("resolved-foo", config.get("p2"));
|
||||
}
|
||||
|
||||
public static class ConfigClass {
|
||||
public String p1 = "";
|
||||
public String p2 = "";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfigAs() {
|
||||
ConfigClass config = handler.getConfig().as(ConfigClass.class);
|
||||
assertEquals("resolved-var", config.p1);
|
||||
assertEquals("resolved-foo", config.p2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolvedConfigAfterThingUpdate() {
|
||||
// Notify the handler: Thing updated with new configuration
|
||||
Thing thing = handler.editThing()
|
||||
.withConfiguration(new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"))).build();
|
||||
handler.thingUpdated(thing);
|
||||
|
||||
assertEquals("resolved-foo", handler.getConfig().get("p1"));
|
||||
assertEquals("resolved-bar", handler.getConfig().get("p2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolvedConfigAfterUpdateThing() {
|
||||
// Action: Update the Thing with new configuration
|
||||
Thing newThing = ThingBuilder.create(THING_TYPE_UID, THING_UID.getId())
|
||||
.withConfiguration(new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"))).build();
|
||||
handler.updateThing(newThing);
|
||||
|
||||
assertEquals("resolved-foo", handler.getConfig().get("p1"));
|
||||
assertEquals("resolved-bar", handler.getConfig().get("p2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolvedConfigAfterConfigurationUpdate() {
|
||||
// Action: Update the configuration
|
||||
Configuration newConfig = new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"));
|
||||
handler.updateConfiguration(newConfig);
|
||||
|
||||
assertEquals("resolved-foo", handler.getConfig().get("p1"));
|
||||
assertEquals("resolved-bar", handler.getConfig().get("p2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleConfigurationUpdate() {
|
||||
handler.handleConfigurationUpdate(Map.of("p1", "${ENV:VAR}_suffix"));
|
||||
|
||||
assertEquals("resolved-var_suffix", handler.getConfig().get("p1"));
|
||||
assertEquals("resolved-foo", handler.getConfig().get("p2"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user