Persistence no default strategies and persistence configuration health check (#4682)

* no default strategy and persistence health
* upgradetool

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege
2025-12-14 17:28:26 +01:00
committed by GitHub
parent 636c476d3e
commit 361d71433d
20 changed files with 288 additions and 82 deletions
@@ -12,6 +12,7 @@
*/ */
package org.openhab.core.io.rest.core.internal.persistence; package org.openhab.core.io.rest.core.internal.persistence;
import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -44,11 +45,13 @@ import javax.ws.rs.core.UriInfo;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role; import org.openhab.core.auth.Role;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource; import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.core.config.ConfigurationService;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
@@ -63,12 +66,13 @@ import org.openhab.core.persistence.PersistenceItemConfiguration;
import org.openhab.core.persistence.PersistenceItemInfo; import org.openhab.core.persistence.PersistenceItemInfo;
import org.openhab.core.persistence.PersistenceManager; import org.openhab.core.persistence.PersistenceManager;
import org.openhab.core.persistence.PersistenceService; import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.PersistenceServiceProblem;
import org.openhab.core.persistence.PersistenceServiceRegistry; import org.openhab.core.persistence.PersistenceServiceRegistry;
import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.config.PersistenceAllConfig;
import org.openhab.core.persistence.dto.ItemHistoryDTO; import org.openhab.core.persistence.dto.ItemHistoryDTO;
import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO;
import org.openhab.core.persistence.dto.PersistenceServiceDTO; import org.openhab.core.persistence.dto.PersistenceServiceDTO;
import org.openhab.core.persistence.dto.PersistenceStrategyDTO;
import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider;
import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; import org.openhab.core.persistence.registry.PersistenceServiceConfiguration;
import org.openhab.core.persistence.registry.PersistenceServiceConfigurationDTOMapper; import org.openhab.core.persistence.registry.PersistenceServiceConfigurationDTOMapper;
@@ -109,6 +113,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
* @author Wouter Born - Migrated to OpenAPI annotations * @author Wouter Born - Migrated to OpenAPI annotations
* @author Mark Herwege - Implement aliases * @author Mark Herwege - Implement aliases
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@Component @Component
@JaxrsResource @JaxrsResource
@@ -136,6 +141,7 @@ public class PersistenceResource implements RESTResource {
private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry; private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry;
private final ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider; private final ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider;
private final TimeZoneProvider timeZoneProvider; private final TimeZoneProvider timeZoneProvider;
private final ConfigurationService configurationService;
@Activate @Activate
public PersistenceResource( // public PersistenceResource( //
@@ -145,7 +151,8 @@ public class PersistenceResource implements RESTResource {
final @Reference PersistenceManager persistenceManager, final @Reference PersistenceManager persistenceManager,
final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry, final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry,
final @Reference ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider, final @Reference ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider,
final @Reference TimeZoneProvider timeZoneProvider) { final @Reference TimeZoneProvider timeZoneProvider,
final @Reference ConfigurationService configurationService) {
this.itemRegistry = itemRegistry; this.itemRegistry = itemRegistry;
this.localeService = localeService; this.localeService = localeService;
this.persistenceServiceRegistry = persistenceServiceRegistry; this.persistenceServiceRegistry = persistenceServiceRegistry;
@@ -153,6 +160,7 @@ public class PersistenceResource implements RESTResource {
this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry; this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry;
this.managedPersistenceServiceConfigurationProvider = managedPersistenceServiceConfigurationProvider; this.managedPersistenceServiceConfigurationProvider = managedPersistenceServiceConfigurationProvider;
this.timeZoneProvider = timeZoneProvider; this.timeZoneProvider = timeZoneProvider;
this.configurationService = configurationService;
} }
@GET @GET
@@ -180,25 +188,11 @@ public class PersistenceResource implements RESTResource {
public Response httpGetPersistenceServiceConfiguration(@Context HttpHeaders headers, public Response httpGetPersistenceServiceConfiguration(@Context HttpHeaders headers,
@Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) { @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) {
PersistenceServiceConfiguration configuration = persistenceServiceConfigurationRegistry.get(serviceId); PersistenceServiceConfiguration configuration = persistenceServiceConfigurationRegistry.get(serviceId);
boolean editable = managedPersistenceServiceConfigurationProvider.get(serviceId) != null;
if (configuration == null) {
PersistenceService service = persistenceServiceRegistry.get(serviceId);
if (service != null) {
List<PersistenceStrategy> strategies = service.getDefaultStrategies();
List<PersistenceItemConfiguration> configs = List
.of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), strategies, null));
Map<String, String> aliases = Map.of();
configuration = new PersistenceServiceConfiguration(serviceId, configs, aliases, strategies, strategies,
List.of());
editable = true;
}
}
if (configuration != null) { if (configuration != null) {
PersistenceServiceConfigurationDTO configurationDTO = PersistenceServiceConfigurationDTOMapper PersistenceServiceConfigurationDTO configurationDTO = PersistenceServiceConfigurationDTOMapper
.map(configuration); .map(configuration);
configurationDTO.editable = editable; configurationDTO.editable = managedPersistenceServiceConfigurationProvider.get(serviceId) != null;
return JSONResponse.createResponse(Status.OK, configurationDTO, null); return JSONResponse.createResponse(Status.OK, configurationDTO, null);
} else { } else {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
@@ -345,6 +339,79 @@ public class PersistenceResource implements RESTResource {
return putItemState(serviceId, itemName, value, time); return putItemState(serviceId, itemName, value, time);
} }
@GET
@RolesAllowed({ Role.ADMIN })
@Produces({ MediaType.APPLICATION_JSON })
@Path("strategysuggestions")
@Operation(operationId = "getPersistenceServiceStrategySuggestions", summary = "Gets a persistence service suggested strategies.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PersistenceStrategyDTO.class), uniqueItems = true))),
@ApiResponse(responseCode = "404", description = "Suggested strategies not found.") })
public Response httpGetPersistenceServiceStrategySuggestions(@Context HttpHeaders headers,
@Parameter(description = "Id of the persistence service.") @QueryParam("serviceId") String serviceId) {
PersistenceService service = persistenceServiceRegistry.get(serviceId);
if (service != null) {
return JSONResponse.createResponse(Status.OK, service.getSuggestedStrategies(), null);
}
return Response.status(Status.NOT_FOUND).build();
}
@GET
@RolesAllowed({ Role.ADMIN })
@Produces({ MediaType.APPLICATION_JSON })
@Path("persistencehealth")
@Operation(operationId = "getPersistenceHealth", summary = "Gets configuration problems with persistence services.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PersistenceServiceProblem.class), uniqueItems = true))) })
public Response httpGetPersistenceHealth(@Context HttpHeaders headers) {
List<PersistenceServiceProblem> persistenceProblems = new ArrayList<>();
Set<PersistenceService> persistenceServices = persistenceServiceRegistry.getAll();
if (persistenceServices.size() > 1) {
try {
Configuration configuration = configurationService.get("org.openhab.persistence");
if (configuration == null || configuration.get("default") == null) {
persistenceProblems.add(new PersistenceServiceProblem(
PersistenceServiceProblem.PERSISTENCE_NO_DEFAULT, null, null, true));
}
} catch (IOException e) {
logger.warn("Unable to retrieve configuration for 'org.openhab.persistence': {}", e.getMessage());
}
}
for (PersistenceService service : persistenceServices) {
String serviceId = service.getId();
PersistenceServiceConfiguration serviceConfig = persistenceServiceConfigurationRegistry.get(serviceId);
if (serviceConfig == null) {
persistenceProblems.add(new PersistenceServiceProblem(PersistenceServiceProblem.PERSISTENCE_NO_CONFIG,
serviceId, null, true));
} else {
boolean editable = managedPersistenceServiceConfigurationProvider.get(serviceId) != null;
List<PersistenceItemConfiguration> configs = serviceConfig.getConfigs();
if (configs.isEmpty()) {
persistenceProblems.add(new PersistenceServiceProblem(
PersistenceServiceProblem.PERSISTENCE_NO_ITEMS, serviceId, null, editable));
} else {
for (PersistenceItemConfiguration config : configs) {
List<PersistenceStrategy> strategies = config.strategies();
List<String> items = config.items().stream()
.map(PersistenceServiceConfigurationDTOMapper::persistenceConfigToString).toList();
if (strategies.isEmpty()) {
persistenceProblems.add(new PersistenceServiceProblem(
PersistenceServiceProblem.PERSISTENCE_NO_STRATEGY, serviceId, items, editable));
} else if (strategies.size() == 1
&& PersistenceStrategy.Globals.RESTORE.equals(strategies.get(0))) {
persistenceProblems.add(new PersistenceServiceProblem(
PersistenceServiceProblem.PERSISTENCE_NO_STORE_STRATEGY, serviceId, items,
editable));
}
}
}
}
}
return JSONResponse.createResponse(Status.OK, persistenceProblems, null);
}
private ZonedDateTime convertTime(String sTime) { private ZonedDateTime convertTime(String sTime) {
DateTimeType dateTime = new DateTimeType(sTime); DateTimeType dateTime = new DateTimeType(sTime);
return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone()); return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone());
@@ -12,11 +12,12 @@
*/ */
package org.openhab.core.io.rest.core.internal.persistence; package org.openhab.core.io.rest.core.internal.persistence;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.time.ZoneId; import java.time.ZoneId;
@@ -36,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.core.config.ConfigurationService;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
@@ -77,13 +79,14 @@ public class PersistenceResourceTest {
private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock; private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock;
private @Mock @NonNullByDefault({}) ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProviderMock; private @Mock @NonNullByDefault({}) ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProviderMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) ConfigurationService configurationServiceMock;
private @Mock @NonNullByDefault({}) Item itemMock; private @Mock @NonNullByDefault({}) Item itemMock;
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
pResource = new PersistenceResource(itemRegistryMock, localeServiceMock, persistenceServiceRegistryMock, pResource = new PersistenceResource(itemRegistryMock, localeServiceMock, persistenceServiceRegistryMock,
persistenceManagerMock, persistenceServiceConfigurationRegistryMock, persistenceManagerMock, persistenceServiceConfigurationRegistryMock,
managedPersistenceServiceConfigurationProviderMock, timeZoneProviderMock); managedPersistenceServiceConfigurationProviderMock, timeZoneProviderMock, configurationServiceMock);
int startValue = 2016; int startValue = 2016;
int endValue = 2018; int endValue = 2018;
@@ -6,9 +6,7 @@ generate persistence "https://openhab.org/model/Persistence"
PersistenceModel: PersistenceModel:
{PersistenceModel} {PersistenceModel}
('Strategies' '{' strategies+=Strategy* ('Strategies' '{' strategies+=Strategy* '}')?
('default' '=' defaults+=[Strategy|ID] (',' defaults+=[Strategy|ID])*)?
'}')?
('Filters' '{' filters+=Filter* '}')? ('Filters' '{' filters+=Filter* '}')?
('Items' '{' configs+=PersistenceConfiguration* '}')? ('Items' '{' configs+=PersistenceConfiguration* '}')?
('Aliases' '{' aliases+=AliasConfiguration* '}')? ('Aliases' '{' aliases+=AliasConfiguration* '}')?
@@ -59,9 +57,8 @@ NotIncludeFilter:
PersistenceConfiguration: PersistenceConfiguration:
items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig) items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig)
(',' items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig))* (',' items+=(AllConfig | ItemConfig | GroupConfig | ItemExcludeConfig | GroupExcludeConfig))*
((':' ('strategy' '=' strategies+=[Strategy|ID] (',' strategies+=[Strategy|ID])*)? (':' ('strategy' '=' strategies+=[Strategy|ID] (',' strategies+=[Strategy|ID])*)
('filter' '=' filters+=[Filter|ID] (',' filters+=[Filter|ID])*)?) ('filter' '=' filters+=[Filter|ID] (',' filters+=[Filter|ID])*)?)
| ';')
; ;
AllConfig: AllConfig:
@@ -116,8 +116,7 @@ public class PersistenceModelManager extends AbstractProvider<PersistenceService
if (model != null) { if (model != null) {
PersistenceServiceConfiguration newConfiguration = new PersistenceServiceConfiguration(serviceName, PersistenceServiceConfiguration newConfiguration = new PersistenceServiceConfiguration(serviceName,
mapConfigs(model.getConfigs()), mapAliases(model.getAliases()), mapConfigs(model.getConfigs()), mapAliases(model.getAliases()),
mapStrategies(model.getDefaults()), mapStrategies(model.getStrategies()), mapStrategies(model.getStrategies()), mapFilters(model.getFilters()));
mapFilters(model.getFilters()));
PersistenceServiceConfiguration oldConfiguration = configurations.put(serviceName, PersistenceServiceConfiguration oldConfiguration = configurations.put(serviceName,
newConfiguration); newConfiguration);
if (oldConfiguration == null) { if (oldConfiguration == null) {
@@ -14,12 +14,11 @@ package org.openhab.core.persistence;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.persistence.internal.PersistenceManagerImpl;
/** /**
* The {@link PersistenceManager} interface is used to communicate between external components (e.g. REST interface) * The {@link PersistenceManager} interface is used to communicate between external components (e.g. REST interface)
* that modify persisted data bypassing the {@link PersistenceManagerImpl}. This is required because forecast jobs might * that modify persisted data bypassing the {@link org.openhab.core.persistence.internal.PersistenceManagerImpl}. This
* need an update when the persisted data changes. * is required because forecast jobs might need an update when the persisted data changes.
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
*/ */
@@ -27,6 +27,7 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* for sending data to an IoT (Internet of Things) service. * for sending data to an IoT (Internet of Things) service.
* *
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@NonNullByDefault @NonNullByDefault
public interface PersistenceService { public interface PersistenceService {
@@ -76,7 +77,22 @@ public interface PersistenceService {
/** /**
* Provides default persistence strategies that are used for all items if no user defined configuration is found. * Provides default persistence strategies that are used for all items if no user defined configuration is found.
* *
* @return The default persistence strategies * This method has been deprecated and {@link #getSuggestedStrategies()} should be used instead. These
* persistence strategies are no longer applied automatically.
*
* @return The suggested persistence strategies
*/ */
List<PersistenceStrategy> getDefaultStrategies(); @Deprecated
default List<PersistenceStrategy> getDefaultStrategies() {
return List.of();
}
/**
* Provides suggested persistence strategies that can be used in the UI as a suggestion for configuration.
*
* @return The suggested persistence strategies
*/
default List<PersistenceStrategy> getSuggestedStrategies() {
return getDefaultStrategies();
}
} }
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2025 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.persistence;
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
/**
* This is a representation of a persistence service configuration problem.
*
* @param reason description for the persistence configuration problem, free text, but one of the public constants can
* be used to allow the consumer (e.g. UI) to tailor the message.
* @param serviceId persistence service
* @param items list of persistence item definitions
* @param editable true if this is a managed service
*
* @author Mark Herwege - Persistence health API endpoint
*/
public record PersistenceServiceProblem(String reason, @Nullable String serviceId, @Nullable List<String> items,
boolean editable) {
// Reasons for persistence configuration problems.
// If one of these constants is used as reason, the UI can use these values to give a more descriptive message.
public static final String PERSISTENCE_NO_DEFAULT = "PERSISTENCE_NO_DEFAULT";
public static final String PERSISTENCE_NO_CONFIG = "PERSISTENCE_SERVICE_NO_CONFIG";
public static final String PERSISTENCE_NO_ITEMS = "PERSISTENCE_SERVICE_NO_ITEMS";
public static final String PERSISTENCE_NO_STRATEGY = "PERSISTENCE_SERVICE_ITEMS_NO_STRATEGY";
public static final String PERSISTENCE_NO_STORE_STRATEGY = "PERSISTENCE_SERVICE_ITEMS_NO_STORE_STRATEGY";
}
@@ -17,18 +17,26 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* The {@link PersistenceServiceConfigurationDTO} is used for transferring persistence service configurations * The {@link PersistenceServiceConfigurationDTO} is used for transferring persistence service configurations
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@NonNullByDefault @NonNullByDefault
public class PersistenceServiceConfigurationDTO { public class PersistenceServiceConfigurationDTO {
public String serviceId = ""; public String serviceId = "";
public Collection<PersistenceItemConfigurationDTO> configs = List.of(); public Collection<PersistenceItemConfigurationDTO> configs = List.of();
public Map<String, String> aliases = Map.of(); public Map<String, String> aliases = Map.of();
public Collection<String> defaults = List.of(); /**
* @deprecated This field is kept to enable migration from previous version storage.
* It should not be removed as this would make automatic upgrading persistence configurations from an
* older version impossible.
*/
@Deprecated
public @Nullable Collection<String> defaults;
public Collection<PersistenceCronStrategyDTO> cronStrategies = List.of(); public Collection<PersistenceCronStrategyDTO> cronStrategies = List.of();
public Collection<PersistenceFilterDTO> thresholdFilters = List.of(); public Collection<PersistenceFilterDTO> thresholdFilters = List.of();
public Collection<PersistenceFilterDTO> timeFilters = List.of(); public Collection<PersistenceFilterDTO> timeFilters = List.of();
@@ -92,6 +92,7 @@ import org.slf4j.LoggerFactory;
* @author Jan N. Klug - Refactored to use service configuration registry * @author Jan N. Klug - Refactored to use service configuration registry
* @author Jan N. Klug - Added time series support * @author Jan N. Klug - Added time series support
* @author Mark Herwege - Added restoring lastState, lastStateChange and lastStateUpdate * @author Mark Herwege - Added restoring lastState, lastStateChange and lastStateUpdate
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@Component(immediate = true, service = PersistenceManager.class) @Component(immediate = true, service = PersistenceManager.class)
@NonNullByDefault @NonNullByDefault
@@ -461,7 +462,7 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
public PersistenceServiceContainer(PersistenceService persistenceService, public PersistenceServiceContainer(PersistenceService persistenceService,
@Nullable PersistenceServiceConfiguration configuration) { @Nullable PersistenceServiceConfiguration configuration) {
this.persistenceService = persistenceService; this.persistenceService = persistenceService;
this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); this.configuration = Objects.requireNonNullElseGet(configuration, this::getEmptyConfig);
} }
public PersistenceService getPersistenceService() { public PersistenceService getPersistenceService() {
@@ -471,13 +472,13 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
/** /**
* Set a new configuration for this persistence service (also cancels all cron jobs) * Set a new configuration for this persistence service (also cancels all cron jobs)
* *
* @param configuration the new {@link PersistenceServiceConfiguration}, if {@code null} the default * @param configuration the new {@link PersistenceServiceConfiguration}, if {@code null} all configuration will
* configuration of the service is used * be removed
*/ */
public void setConfiguration(@Nullable PersistenceServiceConfiguration configuration) { public void setConfiguration(@Nullable PersistenceServiceConfiguration configuration) {
cancelPersistJobs(); cancelPersistJobs();
cancelForecastJobs(); cancelForecastJobs();
this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); this.configuration = Objects.requireNonNullElseGet(configuration, this::getEmptyConfig);
strategyCache.clear(); strategyCache.clear();
} }
@@ -489,25 +490,18 @@ public class PersistenceManagerImpl implements ItemRegistryChangeListener, State
*/ */
public Stream<PersistenceItemConfiguration> getMatchingConfigurations(PersistenceStrategy strategy) { public Stream<PersistenceItemConfiguration> getMatchingConfigurations(PersistenceStrategy strategy) {
return Objects.requireNonNull(strategyCache.computeIfAbsent(strategy, s -> { return Objects.requireNonNull(strategyCache.computeIfAbsent(strategy, s -> {
boolean matchesDefaultStrategies = configuration.getDefaults().contains(strategy);
return configuration.getConfigs().stream() return configuration.getConfigs().stream()
.filter(itemConfig -> itemConfig.strategies().contains(strategy) .filter(itemConfig -> itemConfig.strategies().contains(strategy)).toList();
|| (itemConfig.strategies().isEmpty() && matchesDefaultStrategies)) })).stream();
.toList();
}).stream());
} }
public @Nullable String getAlias(Item item) { public @Nullable String getAlias(Item item) {
return configuration.getAliases().get(item.getName()); return configuration.getAliases().get(item.getName());
} }
private PersistenceServiceConfiguration getDefaultConfig() { private PersistenceServiceConfiguration getEmptyConfig() {
List<PersistenceStrategy> strategies = persistenceService.getDefaultStrategies(); return new PersistenceServiceConfiguration(persistenceService.getId(), List.of(), Map.of(), List.of(),
List<PersistenceItemConfiguration> configs = List List.of());
.of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), strategies, null));
Map<String, String> aliases = Map.of();
return new PersistenceServiceConfiguration(persistenceService.getId(), configs, aliases, strategies,
strategies, List.of());
} }
/** /**
@@ -27,23 +27,22 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Implement aliases * @author Mark Herwege - Implement aliases
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@NonNullByDefault @NonNullByDefault
public class PersistenceServiceConfiguration implements Identifiable<String> { public class PersistenceServiceConfiguration implements Identifiable<String> {
private final String serviceId; private final String serviceId;
private final List<PersistenceItemConfiguration> configs; private final List<PersistenceItemConfiguration> configs;
private final Map<String, String> aliases; private final Map<String, String> aliases;
private final List<PersistenceStrategy> defaults;
private final List<PersistenceStrategy> strategies; private final List<PersistenceStrategy> strategies;
private final List<PersistenceFilter> filters; private final List<PersistenceFilter> filters;
public PersistenceServiceConfiguration(String serviceId, Collection<PersistenceItemConfiguration> configs, public PersistenceServiceConfiguration(String serviceId, Collection<PersistenceItemConfiguration> configs,
Map<String, String> aliases, Collection<PersistenceStrategy> defaults, Map<String, String> aliases, Collection<PersistenceStrategy> strategies,
Collection<PersistenceStrategy> strategies, Collection<PersistenceFilter> filters) { Collection<PersistenceFilter> filters) {
this.serviceId = serviceId; this.serviceId = serviceId;
this.configs = List.copyOf(configs); this.configs = List.copyOf(configs);
this.aliases = Map.copyOf(aliases); this.aliases = Map.copyOf(aliases);
this.defaults = List.copyOf(defaults);
this.strategies = List.copyOf(strategies); this.strategies = List.copyOf(strategies);
this.filters = List.copyOf(filters); this.filters = List.copyOf(filters);
} }
@@ -71,15 +70,6 @@ public class PersistenceServiceConfiguration implements Identifiable<String> {
return aliases; return aliases;
} }
/**
* Get the default strategies.
*
* @return an unmodifiable list of the default strategies
*/
public List<PersistenceStrategy> getDefaults() {
return defaults;
}
/** /**
* Get all defined strategies. * Get all defined strategies.
* *
@@ -47,6 +47,7 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy;
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
* @author Mark Herwege - Implement aliases * @author Mark Herwege - Implement aliases
* @author Mark Herwege - Make default strategy to be only a configuration suggestion
*/ */
@NonNullByDefault @NonNullByDefault
public class PersistenceServiceConfigurationDTOMapper { public class PersistenceServiceConfigurationDTOMapper {
@@ -62,8 +63,6 @@ public class PersistenceServiceConfigurationDTOMapper {
dto.configs = persistenceServiceConfiguration.getConfigs().stream() dto.configs = persistenceServiceConfiguration.getConfigs().stream()
.map(PersistenceServiceConfigurationDTOMapper::mapPersistenceItemConfig).toList(); .map(PersistenceServiceConfigurationDTOMapper::mapPersistenceItemConfig).toList();
dto.aliases = Map.copyOf(persistenceServiceConfiguration.getAliases()); dto.aliases = Map.copyOf(persistenceServiceConfiguration.getAliases());
dto.defaults = persistenceServiceConfiguration.getDefaults().stream().map(PersistenceStrategy::getName)
.toList();
dto.cronStrategies = filterList(persistenceServiceConfiguration.getStrategies(), PersistenceCronStrategy.class, dto.cronStrategies = filterList(persistenceServiceConfiguration.getStrategies(), PersistenceCronStrategy.class,
PersistenceServiceConfigurationDTOMapper::mapPersistenceCronStrategy); PersistenceServiceConfigurationDTOMapper::mapPersistenceCronStrategy);
dto.thresholdFilters = filterList(persistenceServiceConfiguration.getFilters(), dto.thresholdFilters = filterList(persistenceServiceConfiguration.getFilters(),
@@ -92,9 +91,6 @@ public class PersistenceServiceConfigurationDTOMapper {
.map(f -> new PersistenceIncludeFilter(f.name, f.lower, f.upper, f.unit, f.inverted))) .map(f -> new PersistenceIncludeFilter(f.name, f.lower, f.upper, f.unit, f.inverted)))
.flatMap(Function.identity()).collect(Collectors.toMap(PersistenceFilter::getName, e -> e)); .flatMap(Function.identity()).collect(Collectors.toMap(PersistenceFilter::getName, e -> e));
List<PersistenceStrategy> defaults = dto.defaults.stream()
.map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList();
List<PersistenceItemConfiguration> configs = dto.configs.stream().map(config -> { List<PersistenceItemConfiguration> configs = dto.configs.stream().map(config -> {
List<PersistenceConfig> items = config.items.stream() List<PersistenceConfig> items = config.items.stream()
.map(PersistenceServiceConfigurationDTOMapper::stringToPersistenceConfig).toList(); .map(PersistenceServiceConfigurationDTOMapper::stringToPersistenceConfig).toList();
@@ -107,7 +103,7 @@ public class PersistenceServiceConfigurationDTOMapper {
Map<String, String> aliases = Map.copyOf(dto.aliases); Map<String, String> aliases = Map.copyOf(dto.aliases);
return new PersistenceServiceConfiguration(dto.serviceId, configs, aliases, defaults, strategyMap.values(), return new PersistenceServiceConfiguration(dto.serviceId, configs, aliases, strategyMap.values(),
filterMap.values()); filterMap.values());
} }
@@ -154,7 +150,7 @@ public class PersistenceServiceConfigurationDTOMapper {
throw new IllegalArgumentException("Filter '" + string + "' unknown for service '" + serviceId + "'"); throw new IllegalArgumentException("Filter '" + string + "' unknown for service '" + serviceId + "'");
} }
private static String persistenceConfigToString(PersistenceConfig config) { public static String persistenceConfigToString(PersistenceConfig config) {
if (config instanceof PersistenceAllConfig) { if (config instanceof PersistenceAllConfig) {
return "*"; return "*";
} else if (config instanceof PersistenceGroupConfig persistenceGroupConfig) { } else if (config instanceof PersistenceGroupConfig persistenceGroupConfig) {
@@ -14,12 +14,11 @@ package org.openhab.core.persistence.registry;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.Registry; import org.openhab.core.common.registry.Registry;
import org.openhab.core.persistence.internal.PersistenceManagerImpl;
/** /**
* The {@link PersistenceServiceConfigurationRegistry} is the central place to store persistence service configurations. * The {@link PersistenceServiceConfigurationRegistry} is the central place to store persistence service configurations.
* Configurations are registered through {@link PersistenceServiceConfigurationProvider}. * Configurations are registered through {@link PersistenceServiceConfigurationProvider}.
* Because the {@link PersistenceManagerImpl} implementation needs to listen to * Because the {@link org.openhab.core.persistence.internal.PersistenceManagerImpl} implementation needs to listen to
* different registries, the {@link PersistenceServiceConfigurationRegistryChangeListener} can be used to add listeners * different registries, the {@link PersistenceServiceConfigurationRegistryChangeListener} can be used to add listeners
* to this registry. * to this registry.
* *
@@ -124,7 +124,7 @@ public class TestCachedValuesPersistenceService implements ModifiablePersistence
} }
@Override @Override
public List<PersistenceStrategy> getDefaultStrategies() { public List<PersistenceStrategy> getSuggestedStrategies() {
return List.of(); return List.of();
} }
@@ -231,7 +231,7 @@ public class TestPersistenceService implements QueryablePersistenceService {
} }
@Override @Override
public List<PersistenceStrategy> getDefaultStrategies() { public List<PersistenceStrategy> getSuggestedStrategies() {
return List.of(); return List.of();
} }
@@ -13,8 +13,7 @@
package org.openhab.core.persistence.filter; package org.openhab.core.persistence.filter;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.Collection; import java.util.Collection;
@@ -47,9 +46,8 @@ import org.openhab.core.types.State;
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault @NonNullByDefault
public class PersistenceEqualsFilterTest { public class PersistenceEqualsFilterTest {
private static final String ITEM_NAME = "itemName";
private @NonNullByDefault({}) @Mock GenericItem item; private @Mock @NonNullByDefault({}) GenericItem item;
@ParameterizedTest @ParameterizedTest
@MethodSource("argumentProvider") @MethodSource("argumentProvider")
@@ -13,7 +13,7 @@
package org.openhab.core.persistence.filter; package org.openhab.core.persistence.filter;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -42,9 +42,8 @@ import org.openhab.core.types.State;
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault @NonNullByDefault
public class PersistenceIncludeFilterTest { public class PersistenceIncludeFilterTest {
private static final String ITEM_NAME = "itemName";
private @NonNullByDefault({}) @Mock GenericItem item; private @Mock @NonNullByDefault({}) GenericItem item;
@ParameterizedTest @ParameterizedTest
@MethodSource("argumentProvider") @MethodSource("argumentProvider")
@@ -574,7 +574,7 @@ public class PersistenceManagerTest {
: List.of(strategy); : List.of(strategy);
PersistenceServiceConfiguration serviceConfiguration = new PersistenceServiceConfiguration(serviceId, PersistenceServiceConfiguration serviceConfiguration = new PersistenceServiceConfiguration(serviceId,
List.of(itemConfiguration), Map.of(), List.of(), strategies, filters); List.of(itemConfiguration), Map.of(), strategies, filters);
manager.added(serviceConfiguration); manager.added(serviceConfiguration);
return serviceConfiguration; return serviceConfiguration;
@@ -625,7 +625,7 @@ public class PersistenceManagerTest {
} }
@Override @Override
public List<PersistenceStrategy> getDefaultStrategies() { public List<PersistenceStrategy> getSuggestedStrategies() {
return List.of(); return List.of();
} }
+5
View File
@@ -37,6 +37,11 @@
<artifactId>org.openhab.core.thing</artifactId> <artifactId>org.openhab.core.thing</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.core.bundles</groupId> <groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.storage.json</artifactId> <artifactId>org.openhab.core.storage.json</artifactId>
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
* @author Jimmy Tanagra - Refactor upgraders into individual classes * @author Jimmy Tanagra - Refactor upgraders into individual classes
* @author Mark Herwege - Added persistence strategy upgrader
*/ */
@NonNullByDefault @NonNullByDefault
public class UpgradeTool { public class UpgradeTool {
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2010-2025 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.tools.internal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO;
import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO;
import org.openhab.core.storage.json.internal.JsonStorage;
import org.openhab.core.tools.Upgrader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PersistenceUpgrader} removes the default persistence strategy.
*
* It upgrades the PersistenceServiceConfiguration database, removing default strategies and setting strategies on each
* configuration that has no strategy defined.
* See <a href="https://github.com/openhab/openhab-core/pull/4682">openhab/openhab-core#4682</a>.
*
* @author Mark Herwege - Initial Contribution
*/
@NonNullByDefault
public class PersistenceUpgrader implements Upgrader {
private final Logger logger = LoggerFactory.getLogger(PersistenceUpgrader.class);
@Override
public String getName() {
return "persistenceCopyDefaultStrategy";
}
@Override
public String getDescription() {
return "Move persistence default strategy configuration to all persistence configuration without strategy defined";
}
@Override
public boolean execute(@Nullable Path userdataPath, @Nullable Path confPath) {
if (userdataPath == null) {
logger.error("{} skipped: no userdata directory found.", getName());
return false;
}
Path persistenceJsonDatabasePath = userdataPath
.resolve(Path.of("jsondb", "org.openhab.core.persistence.PersistenceServiceConfiguration.json"));
logger.info("Setting default strategy on persistence configurations without strategy '{}'",
persistenceJsonDatabasePath);
if (!Files.isWritable(persistenceJsonDatabasePath)) {
logger.error("Cannot access persistence configuration database '{}', check path and access rights.",
persistenceJsonDatabasePath);
return false;
}
JsonStorage<PersistenceServiceConfigurationDTO> persistenceStorage = new JsonStorage<>(
persistenceJsonDatabasePath.toFile(), null, 5, 0, 0, List.of());
List.copyOf(persistenceStorage.getKeys()).forEach(serviceId -> {
PersistenceServiceConfigurationDTO serviceConfigDTO = Objects
.requireNonNull(persistenceStorage.get(serviceId));
Collection<String> defaults = serviceConfigDTO.defaults;
if (defaults != null) {
Collection<PersistenceItemConfigurationDTO> configs = serviceConfigDTO.configs;
configs.forEach(config -> {
Collection<String> strategies = config.strategies;
if (strategies.isEmpty()) {
config.strategies = defaults;
}
});
serviceConfigDTO.defaults = null;
persistenceStorage.put(serviceId, serviceConfigDTO);
logger.info("{}: updated strategy configurations and removed default strategies", serviceId);
}
});
persistenceStorage.flush();
return true;
}
}