diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java index ef1945f8bbc..fe0f01a6af0 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java @@ -20,6 +20,7 @@ import java.util.Locale; import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -33,6 +34,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -57,7 +59,12 @@ import org.openhab.core.persistence.PersistenceServiceRegistry; import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.dto.ItemHistoryDTO; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; import org.openhab.core.persistence.dto.PersistenceServiceDTO; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationDTOMapper; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.osgi.service.component.annotations.Activate; @@ -114,6 +121,8 @@ public class PersistenceResource implements RESTResource { private final ItemRegistry itemRegistry; private final LocaleService localeService; private final PersistenceServiceRegistry persistenceServiceRegistry; + private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry; + private final ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider; private final TimeZoneProvider timeZoneProvider; @Activate @@ -121,10 +130,14 @@ public PersistenceResource( // final @Reference ItemRegistry itemRegistry, // final @Reference LocaleService localeService, final @Reference PersistenceServiceRegistry persistenceServiceRegistry, + final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry, + final @Reference ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProvider, final @Reference TimeZoneProvider timeZoneProvider) { this.itemRegistry = itemRegistry; this.localeService = localeService; this.persistenceServiceRegistry = persistenceServiceRegistry; + this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry; + this.managedPersistenceServiceConfigurationProvider = managedPersistenceServiceConfigurationProvider; this.timeZoneProvider = timeZoneProvider; } @@ -142,6 +155,96 @@ public Response httpGetPersistenceServices(@Context HttpHeaders headers, return Response.ok(responseObject).build(); } + @GET + @RolesAllowed({ Role.ADMIN }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "getPersistenceServiceConfiguration", summary = "Gets a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = PersistenceServiceConfigurationDTO.class))), + @ApiResponse(responseCode = "404", description = "Service configuration not found.") }) + public Response httpGetPersistenceServiceConfiguration(@Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) { + PersistenceServiceConfiguration configuration = persistenceServiceConfigurationRegistry.get(serviceId); + + if (configuration != null) { + PersistenceServiceConfigurationDTO configurationDTO = PersistenceServiceConfigurationDTOMapper + .map(configuration); + configurationDTO.editable = managedPersistenceServiceConfigurationProvider.get(serviceId) != null; + return JSONResponse.createResponse(Status.OK, configurationDTO, null); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @PUT + @RolesAllowed({ Role.ADMIN }) + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "putPersistenceServiceConfiguration", summary = "Sets a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = PersistenceServiceConfigurationDTO.class))), + @ApiResponse(responseCode = "201", description = "PersistenceServiceConfiguration created."), + @ApiResponse(responseCode = "400", description = "Payload invalid."), + @ApiResponse(responseCode = "405", description = "PersistenceServiceConfiguration not editable.") }) + public Response httpPutPersistenceServiceConfiguration(@Context UriInfo uriInfo, @Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId, + @Parameter(description = "service configuration", required = true) @Nullable PersistenceServiceConfigurationDTO serviceConfigurationDTO) { + if (serviceConfigurationDTO == null) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Payload must not be null."); + } + if (!serviceId.equals(serviceConfigurationDTO.serviceId)) { + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "serviceId in payload '" + + serviceConfigurationDTO.serviceId + "' differs from serviceId in URL '" + serviceId + "'"); + } + + PersistenceServiceConfiguration persistenceServiceConfiguration; + try { + persistenceServiceConfiguration = PersistenceServiceConfigurationDTOMapper.map(serviceConfigurationDTO); + } catch (IllegalArgumentException e) { + logger.warn("Received HTTP PUT request at '{}' with an invalid payload: '{}'.", uriInfo.getPath(), + e.getMessage()); + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, e.getMessage()); + } + + if (persistenceServiceConfigurationRegistry.get(serviceId) == null) { + managedPersistenceServiceConfigurationProvider.add(persistenceServiceConfiguration); + return JSONResponse.createResponse(Status.CREATED, serviceConfigurationDTO, null); + } else if (managedPersistenceServiceConfigurationProvider.get(serviceId) != null) { + // item already exists as a managed item, update it + managedPersistenceServiceConfigurationProvider.update(persistenceServiceConfiguration); + return JSONResponse.createResponse(Status.OK, serviceConfigurationDTO, null); + } else { + // Configuration exists but cannot be updated + logger.warn("Cannot update existing persistence service configuration '{}', because is not managed.", + serviceId); + return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, + "Cannot update non-managed persistence service configuration " + serviceId); + } + } + + @DELETE + @RolesAllowed({ Role.ADMIN }) + @Path("{serviceId: [a-zA-Z0-9]+}") + @Operation(operationId = "deletePersistenceServiceConfiguration", summary = "Deletes a persistence service configuration.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Persistence service configuration not found."), + @ApiResponse(responseCode = "405", description = "Persistence service configuration not editable.") }) + public Response httpDeletePersistenceServiceConfiguration(@Context UriInfo uriInfo, @Context HttpHeaders headers, + @Parameter(description = "Id of the persistence service.") @PathParam("serviceId") String serviceId) { + if (persistenceServiceConfigurationRegistry.get(serviceId) == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + if (managedPersistenceServiceConfigurationProvider.remove(serviceId) == null) { + return Response.status(Status.METHOD_NOT_ALLOWED).build(); + } else { + return Response.ok().build(); + } + } + @GET @RolesAllowed({ Role.ADMIN }) @Path("/items") @@ -238,7 +341,7 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, protected @Nullable ItemHistoryDTO createDTO(@Nullable String serviceId, String itemName, @Nullable String timeBegin, @Nullable String timeEnd, int pageNumber, int pageLength, boolean boundary) { // If serviceId is null, then use the default service - PersistenceService service = null; + PersistenceService service; String effectiveServiceId = serviceId != null ? serviceId : persistenceServiceRegistry.getDefaultId(); service = persistenceServiceRegistry.get(effectiveServiceId); @@ -283,9 +386,9 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, } Iterable result; - State state = null; + State state; - Long quantity = 0l; + long quantity = 0L; ItemHistoryDTO dto = new ItemHistoryDTO(); dto.name = itemName; @@ -363,7 +466,7 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, /** * Gets a list of persistence services currently configured in the system * - * @return list of persistence services as {@link ServiceBean} + * @return list of persistence services */ private List getPersistenceServiceList(Locale locale) { List dtoList = new ArrayList<>(); @@ -389,7 +492,7 @@ private List getPersistenceServiceList(Locale locale) { private Response getServiceItemList(@Nullable String serviceId) { // If serviceId is null, then use the default service - PersistenceService service = null; + PersistenceService service; if (serviceId == null) { service = persistenceServiceRegistry.getDefault(); } else { diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java index 6cf040ab7f5..7ad448a9ff2 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResourceTest.java @@ -39,6 +39,8 @@ import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.dto.ItemHistoryDTO; import org.openhab.core.persistence.dto.ItemHistoryDTO.HistoryDataBean; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; import org.openhab.core.types.State; /** @@ -58,11 +60,14 @@ public class PersistenceResourceTest { private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) LocaleService localeServiceMock; private @Mock @NonNullByDefault({}) PersistenceServiceRegistry persistenceServiceRegistryMock; + private @Mock @NonNullByDefault({}) PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock; + private @Mock @NonNullByDefault({}) ManagedPersistenceServiceConfigurationProvider managedPersistenceServiceConfigurationProviderMock; private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; @BeforeEach public void beforeEach() { pResource = new PersistenceResource(itemRegistryMock, localeServiceMock, persistenceServiceRegistryMock, + persistenceServiceConfigurationRegistryMock, managedPersistenceServiceConfigurationProviderMock, timeZoneProviderMock); int startValue = 2016; diff --git a/bundles/org.openhab.core.model.persistence/bnd.bnd b/bundles/org.openhab.core.model.persistence/bnd.bnd index caef2b8c1c8..c5067b3cc13 100644 --- a/bundles/org.openhab.core.model.persistence/bnd.bnd +++ b/bundles/org.openhab.core.model.persistence/bnd.bnd @@ -21,6 +21,8 @@ Import-Package: \ org.openhab.core.persistence,\ org.openhab.core.persistence.config,\ org.openhab.core.persistence.strategy,\ + org.openhab.core.persistence.filter,\ + org.openhab.core.persistence.registry,\ org.openhab.core.types,\ org.openhab.core.model.core,\ com.google.common.*;version="14",\ diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext index 9bdb11e1137..4e34511c2e1 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/Persistence.xtext @@ -31,7 +31,7 @@ FilterDetails: ; ThresholdFilter: - '>' value=DECIMAL percent?='%' + '>' value=DECIMAL unit=STRING ; TimeFilter: diff --git a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java index 626da1d702c..3531eb4976c 100644 --- a/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java +++ b/bundles/org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/internal/PersistenceModelManager.java @@ -12,10 +12,15 @@ */ package org.openhab.core.model.persistence.internal; +import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.emf.ecore.EObject; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractProvider; import org.openhab.core.model.core.EventType; import org.openhab.core.model.core.ModelRepository; import org.openhab.core.model.core.ModelRepositoryChangeListener; @@ -27,18 +32,24 @@ import org.openhab.core.model.persistence.persistence.PersistenceConfiguration; import org.openhab.core.model.persistence.persistence.PersistenceModel; import org.openhab.core.model.persistence.persistence.Strategy; -import org.openhab.core.persistence.PersistenceFilter; +import org.openhab.core.model.persistence.persistence.ThresholdFilter; +import org.openhab.core.model.persistence.persistence.TimeFilter; import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceManager; import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; import org.openhab.core.persistence.config.PersistenceAllConfig; import org.openhab.core.persistence.config.PersistenceConfig; import org.openhab.core.persistence.config.PersistenceGroupConfig; import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.filter.PersistenceTimeFilter; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationProvider; import org.openhab.core.persistence.strategy.PersistenceCronStrategy; import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; /** @@ -47,75 +58,71 @@ * * @author Kai Kreuzer - Initial contribution * @author Markus Rathgeb - Move non-model logic to core.persistence + * @author Jan N. Klug - Refactored to {@link PersistenceServiceConfigurationProvider} */ -@Component(immediate = true) -public class PersistenceModelManager implements ModelRepositoryChangeListener { - - private ModelRepository modelRepository; - - private PersistenceManager manager; - - public PersistenceModelManager() { - } +@Component(immediate = true, service = PersistenceServiceConfigurationProvider.class) +@NonNullByDefault +public class PersistenceModelManager extends AbstractProvider + implements ModelRepositoryChangeListener, PersistenceServiceConfigurationProvider { + private final Map configurations = new ConcurrentHashMap<>(); + private final ModelRepository modelRepository; + + @Activate + public PersistenceModelManager(@Reference ModelRepository modelRepository) { + this.modelRepository = modelRepository; - protected void activate() { modelRepository.addModelRepositoryChangeListener(this); - for (String modelName : modelRepository.getAllModelNamesOfType("persist")) { - addModel(modelName); - } + modelRepository.getAllModelNamesOfType("persist") + .forEach(modelName -> modelChanged(modelName, EventType.ADDED)); } + @Deactivate protected void deactivate() { modelRepository.removeModelRepositoryChangeListener(this); - for (String modelName : modelRepository.getAllModelNamesOfType("persist")) { - removeModel(modelName); - } - } - - @Reference - protected void setModelRepository(ModelRepository modelRepository) { - this.modelRepository = modelRepository; - } - - protected void unsetModelRepository(ModelRepository modelRepository) { - this.modelRepository = null; - } - - @Reference - protected void setPersistenceManager(final PersistenceManager manager) { - this.manager = manager; - } - - protected void unsetPersistenceManager(final PersistenceManager manager) { - this.manager = null; + modelRepository.getAllModelNamesOfType("persist") + .forEach(modelName -> modelChanged(modelName, EventType.REMOVED)); } @Override public void modelChanged(String modelName, EventType type) { if (modelName.endsWith(".persist")) { - if (type == EventType.REMOVED || type == EventType.MODIFIED) { - removeModel(modelName); - } - if (type == EventType.ADDED || type == EventType.MODIFIED) { - addModel(modelName); - } - } - } - - private void addModel(String modelName) { - final PersistenceModel model = (PersistenceModel) modelRepository.getModel(modelName); - if (model != null) { String serviceName = serviceName(modelName); - manager.addConfig(serviceName, new PersistenceServiceConfiguration(mapConfigs(model.getConfigs()), - mapStrategies(model.getDefaults()), mapStrategies(model.getStrategies()))); + if (type == EventType.REMOVED) { + PersistenceServiceConfiguration removed = configurations.remove(serviceName); + notifyListenersAboutRemovedElement(removed); + } else { + final PersistenceModel model = (PersistenceModel) modelRepository.getModel(modelName); + + if (model != null) { + PersistenceServiceConfiguration newConfiguration = new PersistenceServiceConfiguration(serviceName, + mapConfigs(model.getConfigs()), mapStrategies(model.getDefaults()), + mapStrategies(model.getStrategies()), mapFilters(model.getFilters())); + PersistenceServiceConfiguration oldConfiguration = configurations.put(serviceName, + newConfiguration); + if (oldConfiguration == null) { + if (type != EventType.ADDED) { + logger.warn( + "Model {} is inconsistent: An updated event was sent, but there is no old configuration. Adding it now.", + modelName); + } + notifyListenersAboutAddedElement(newConfiguration); + } else { + if (type != EventType.MODIFIED) { + logger.warn( + "Model {} is inconsistent: An added event was sent, but there is an old configuration. Replacing it now.", + modelName); + } + notifyListenersAboutUpdatedElement(oldConfiguration, newConfiguration); + } + } else { + logger.error( + "The model repository reported a {} event for model '{}' but the model could not be found in the repository. ", + type, modelName); + } + } } } - private void removeModel(String modelName) { - String serviceName = serviceName(modelName); - manager.removeConfig(serviceName); - } - private String serviceName(String modelName) { return modelName.substring(0, modelName.length() - ".persist".length()); } @@ -168,6 +175,19 @@ private List mapFilters(List filters) { } private PersistenceFilter mapFilter(Filter filter) { - return new PersistenceFilter(); + if (filter.getDefinition() instanceof TimeFilter) { + TimeFilter timeFilter = (TimeFilter) filter.getDefinition(); + return new PersistenceTimeFilter(filter.getName(), timeFilter.getValue(), timeFilter.getUnit()); + } else if (filter.getDefinition() instanceof ThresholdFilter) { + ThresholdFilter thresholdFilter = (ThresholdFilter) filter.getDefinition(); + return new PersistenceThresholdFilter(filter.getName(), thresholdFilter.getValue(), + thresholdFilter.getUnit()); + } + throw new IllegalArgumentException("Unknown filter type " + filter.getClass()); + } + + @Override + public Collection getAll() { + return List.copyOf(configurations.values()); } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java index aaf2b5a458c..132660ea71b 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/FilterCriteria.java @@ -14,6 +14,8 @@ import java.time.ZonedDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.State; /** @@ -36,6 +38,7 @@ * @author Lyubomir Papazov - Deprecate methods using java.util and add methods * that use Java8's ZonedDateTime */ +@NonNullByDefault public class FilterCriteria { /** Enumeration with all possible compare options */ @@ -65,13 +68,13 @@ public enum Ordering { } /** filter result to only contain entries for the given item */ - private String itemName; + private @Nullable String itemName; /** filter result to only contain entries that are equal to or after the given datetime */ - private ZonedDateTime beginDate; + private @Nullable ZonedDateTime beginDate; /** filter result to only contain entries that are equal to or before the given datetime */ - private ZonedDateTime endDate; + private @Nullable ZonedDateTime endDate; /** return the result list from starting index pageNumber*pageSize only */ private int pageNumber = 0; @@ -86,17 +89,17 @@ public enum Ordering { private Ordering ordering = Ordering.DESCENDING; /** Filter result to only contain entries that evaluate to true with the given operator and state */ - private State state; + private @Nullable State state; - public String getItemName() { + public @Nullable String getItemName() { return itemName; } - public ZonedDateTime getBeginDate() { + public @Nullable ZonedDateTime getBeginDate() { return beginDate; } - public ZonedDateTime getEndDate() { + public @Nullable ZonedDateTime getEndDate() { return endDate; } @@ -116,7 +119,7 @@ public Ordering getOrdering() { return ordering; } - public State getState() { + public @Nullable State getState() { return state; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java index aab98c2ef56..a6c9bc24dd5 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/HistoricItem.java @@ -15,7 +15,6 @@ import java.time.ZonedDateTime; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; import org.openhab.core.types.State; /** @@ -23,7 +22,8 @@ * with a certain state at a given point in time. * *

- * Note that this interface does not extend {@link Item} as the persistence services could not provide an implementation + * Note that this interface does not extend {@link org.openhab.core.items.Item} as the persistence services could not + * provide an implementation * that correctly implement getAcceptedXTypes() and getGroupNames(). * * @author Kai Kreuzer - Initial contribution diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java index 10c577a36ed..2feb4f4660f 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/ModifiablePersistenceService.java @@ -49,11 +49,11 @@ public interface ModifiablePersistenceService extends QueryablePersistenceServic /** * Removes data associated with an item from a persistence service. * If all data is removed for the specified item, the persistence service should free any resources associated with - * the item (eg. remove any tables or delete files from the storage). + * the item (e.g. remove any tables or delete files from the storage). * * @param filter the filter to apply to the data removal. ItemName can not be null. * @return true if the query executed successfully - * @throws {@link IllegalArgumentException} if item name is null. + * @throws IllegalArgumentException if item name is null. */ boolean remove(FilterCriteria filter) throws IllegalArgumentException; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java index 30b5f935595..36d462333d4 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceItemConfiguration.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.filter.PersistenceFilter; import org.openhab.core.persistence.strategy.PersistenceStrategy; /** @@ -26,12 +27,8 @@ * @author Markus Rathgeb - Initial contribution */ @NonNullByDefault -public class PersistenceItemConfiguration { - - private final List items; - private final @Nullable String alias; - private final List strategies; - private final List filters; +public record PersistenceItemConfiguration(List items, @Nullable String alias, + List strategies, List filters) { public PersistenceItemConfiguration(final List items, @Nullable final String alias, @Nullable final List strategies, @Nullable final List filters) { @@ -40,26 +37,4 @@ public PersistenceItemConfiguration(final List items, @Nullab this.strategies = Objects.requireNonNullElse(strategies, List.of()); this.filters = Objects.requireNonNullElse(filters, List.of()); } - - public List getItems() { - return items; - } - - public @Nullable String getAlias() { - return alias; - } - - public List getStrategies() { - return strategies; - } - - public List getFilters() { - return filters; - } - - @Override - public String toString() { - return String.format("%s [items=%s, alias=%s, strategies=%s, filters=%s]", getClass().getSimpleName(), items, - alias, strategies, filters); - } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java deleted file mode 100644 index 6e8d5b80c5a..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceManager.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * A persistence manager service which could be used to start event handling or supply configuration for persistence - * services. - * - * @author Markus Rathgeb - Initial contribution - */ -@NonNullByDefault -public interface PersistenceManager { - /** - * Add a configuration for a persistence service. - * - * @param dbId the database id used by the persistence service - * @param config the configuration of the persistence service - */ - void addConfig(String dbId, PersistenceServiceConfiguration config); - - /** - * Remove a configuration for a persistence service. - * - * @param dbId the database id used by the persistence service - */ - void removeConfig(String dbId); -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java index 6b8c2dc56c3..f07bdcfec6f 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/QueryablePersistenceService.java @@ -15,7 +15,6 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; /** * A queryable persistence service which can be used to store and retrieve @@ -37,7 +36,8 @@ public interface QueryablePersistenceService extends PersistenceService { /** * Returns a set of {@link PersistenceItemInfo} about items that are stored in the persistence service. This allows - * the persistence service to return information about items that are no long available as an {@link Item} in + * the persistence service to return information about items that are no long available as an + * {@link org.openhab.core.items.Item} in * openHAB. If it is not possible to retrieve the information an empty set should be returned. * * @return a set of information about the persisted items diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java index 2bf26b1e9bf..949805fe686 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java @@ -45,9 +45,9 @@ public ItemHistoryDTO() { public void addData(Long time, State state) { HistoryDataBean newVal = new HistoryDataBean(); newVal.time = time; - if (state instanceof QuantityType type) { + if (state instanceof QuantityType quantityState) { // we strip the unit from the state, since historic item states are expected to be all in the default unit - newVal.state = type.toBigDecimal().toString(); + newVal.state = quantityState.toBigDecimal().toString(); } else { newVal.state = state.toString(); } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java similarity index 61% rename from bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java rename to bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java index f7ae88e1f3c..50c098618c7 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceFilter.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceCronStrategyDTO.java @@ -10,16 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.persistence; +package org.openhab.core.persistence.dto; import org.eclipse.jdt.annotation.NonNullByDefault; /** + * The {@link PersistenceCronStrategyDTO} is used for transferring persistence cron + * strategies * - * - * @author Markus Rathgeb - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class PersistenceFilter { - +public class PersistenceCronStrategyDTO { + public String name = ""; + public String cronExpression = ""; } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java new file mode 100644 index 00000000000..d59bc9a480d --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceFilterDTO.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2023 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.dto; + +import java.math.BigDecimal; + +/** + * The {@link org.openhab.core.persistence.dto.PersistenceFilterDTO} is used for transferring persistence filter + * configurations + * + * @author Jan N. Klug - Initial contribution + */ +public class PersistenceFilterDTO { + public String name = ""; + public BigDecimal value = BigDecimal.ZERO; + public String unit = ""; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java new file mode 100644 index 00000000000..e1b0e082e60 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceItemConfigurationDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 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.dto; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO} is used for transferring persistence + * item configurations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceItemConfigurationDTO { + public Collection items = List.of(); + public Collection strategies = List.of(); + public Collection filters = List.of(); + public @Nullable String alias; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java new file mode 100644 index 00000000000..bc796a36871 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceConfigurationDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2023 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.dto; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceServiceConfigurationDTO} is used for transferring persistence service configurations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceServiceConfigurationDTO { + public String serviceId = ""; + public Collection configs = List.of(); + public Collection defaults = List.of(); + public Collection cronStrategies = List.of(); + public Collection thresholdFilters = List.of(); + public Collection timeFilters = List.of(); + + public boolean editable = false; +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java new file mode 100644 index 00000000000..67bc881c0f4 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceStrategyDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 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.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceStrategyDTO} is used for transferring persistence strategies. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceStrategyDTO { + public String type; + public String configuration; + + // do not remove - needed by GSON + PersistenceStrategyDTO() { + this("", ""); + } + + public PersistenceStrategyDTO(String type, String configuration) { + this.type = type; + this.configuration = configuration; + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java index 4b2bd7d7386..478587314f4 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java @@ -1208,15 +1208,10 @@ public static long countStateChangesBetween(Item item, ZonedDateTime begin, @Nul } private static @Nullable PersistenceService getService(String serviceId) { - PersistenceService service = null; if (registry != null) { - if (serviceId != null) { - service = registry.get(serviceId); - } else { - service = registry.getDefault(); - } + return serviceId != null ? registry.get(serviceId) : registry.getDefault(); } - return service; + return null; } private static @Nullable String getDefaultServiceId() { diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.java new file mode 100644 index 00000000000..8ee2c8559c2 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceFilter.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2023 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.filter; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; + +/** + * The {@link PersistenceFilter} is the base class for implementing persistence filters. + * + * @author Markus Rathgeb - Initial contribution + */ +@NonNullByDefault +public abstract class PersistenceFilter { + private final String name; + + public PersistenceFilter(final String name) { + this.name = name; + } + + /** + * Get the name of this filter + * + * @return a unique name + */ + public String getName() { + return name; + } + + /** + * Apply this filter to an item + * + * @param item the item to check + * @return true if the filter allows persisting this value + */ + public abstract boolean apply(Item item); + + /** + * Notify filter that item was persisted + * + * @param item the persisted item + */ + public abstract void persisted(Item item); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + name.hashCode(); + return result; + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof PersistenceFilter)) { + return false; + } + final PersistenceFilter other = (PersistenceFilter) obj; + return Objects.equals(name, other.name); + } + + @Override + public String toString() { + return String.format("%s [name=%s]", getClass().getSimpleName(), name); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java new file mode 100644 index 00000000000..efd3d6d968c --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceThresholdFilter.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2023 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.filter; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; + +import javax.measure.UnconvertibleException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PersistenceThresholdFilter} is a filter to prevent persistence based on a threshold. + * + * The filter returns {@code false} if the new value deviates by less than {@link #value}. If unit is "%" is + * {@code true}, the filter returns {@code false} if the relative deviation is less than {@link #value}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceThresholdFilter extends PersistenceFilter { + private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); + + private final Logger logger = LoggerFactory.getLogger(PersistenceThresholdFilter.class); + + private final BigDecimal value; + private final String unit; + + private final transient Map valueCache = new HashMap<>(); + + public PersistenceThresholdFilter(String name, BigDecimal value, String unit) { + super(name); + this.value = value; + this.unit = unit; + } + + public BigDecimal getValue() { + return value; + } + + public String getUnit() { + return unit; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean apply(Item item) { + String itemName = item.getName(); + State state = item.getState(); + if (!(state instanceof DecimalType || state instanceof QuantityType)) { + return true; + } + + State cachedState = valueCache.get(itemName); + + if (cachedState == null || !state.getClass().equals(cachedState.getClass())) { + return true; + } + + if (state instanceof DecimalType) { + BigDecimal oldState = ((DecimalType) cachedState).toBigDecimal(); + BigDecimal delta = oldState.subtract(((DecimalType) state).toBigDecimal()); + if ("%".equals(unit) && !BigDecimal.ZERO.equals(oldState)) { + delta = delta.multiply(HUNDRED).divide(oldState, 2, RoundingMode.HALF_UP); + } + return delta.abs().compareTo(value) > 0; + } else { + try { + QuantityType oldState = (QuantityType) cachedState; + QuantityType delta = oldState.subtract((QuantityType) state); + if ("%".equals(unit)) { + if (BigDecimal.ZERO.equals(oldState.toBigDecimal())) { + // value is different and old value is 0 -> always above relative threshold + return true; + } else { + // calculate percent + delta = delta.multiply(HUNDRED).divide(oldState); + } + } else if (!unit.isBlank()) { + // consider unit only if not relative threshold + delta = delta.toUnit(unit); + if (delta == null) { + throw new UnconvertibleException(""); + } + } + + return delta.toBigDecimal().abs().compareTo(value) > 0; + } catch (UnconvertibleException e) { + logger.warn("Cannot compare {} to {}", cachedState, state); + return true; + } + } + } + + @Override + public void persisted(Item item) { + valueCache.put(item.getName(), item.getState()); + } + + @Override + public String toString() { + return String.format("%s [name=%s, value=%s, unit=%s]", getClass().getSimpleName(), getName(), value, unit); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java new file mode 100644 index 00000000000..c3edcfdc6f3 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/filter/PersistenceTimeFilter.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2023 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.filter; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Item; + +/** + * The {@link PersistenceTimeFilter} is a filter to prevent persistence base on intervals. + * + * The filter returns {@link false} if the time between now and the time of the last persisted value is less than + * {@link #duration} {@link #unit} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceTimeFilter extends PersistenceFilter { + private final int value; + private final String unit; + + private transient @Nullable Duration duration; + private final transient Map nextPersistenceTimes = new HashMap<>(); + + public PersistenceTimeFilter(String name, int value, String unit) { + super(name); + this.value = value; + this.unit = unit; + } + + public int getValue() { + return value; + } + + public String getUnit() { + return unit; + } + + @Override + public boolean apply(Item item) { + String itemName = item.getName(); + + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime nextPersistenceTime = nextPersistenceTimes.get(itemName); + + return nextPersistenceTime == null || !now.isBefore(nextPersistenceTime); + } + + @Override + public void persisted(Item item) { + Duration duration = this.duration; + if (duration == null) { + duration = switch (unit) { + case "m" -> Duration.of(value, ChronoUnit.MINUTES); + case "h" -> Duration.of(value, ChronoUnit.HOURS); + case "d" -> Duration.of(value, ChronoUnit.DAYS); + default -> Duration.of(value, ChronoUnit.SECONDS); + }; + + this.duration = duration; + } + nextPersistenceTimes.put(item.getName(), ZonedDateTime.now().plus(duration)); + } + + @Override + public String toString() { + return String.format("%s [name=%s, value=%s, unit=%s]", getClass().getSimpleName(), getName(), value, unit); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java deleted file mode 100644 index 586525ad9a2..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistItemsJob.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.internal; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.items.Item; -import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; -import org.openhab.core.persistence.strategy.PersistenceStrategy; -import org.openhab.core.scheduler.SchedulerRunnable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of a persistence job that could be executed e.g. for specific cron expressions. - * - * @author Kai Kreuzer - Initial contribution - * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. - */ -@NonNullByDefault -public class PersistItemsJob implements SchedulerRunnable { - - private final Logger logger = LoggerFactory.getLogger(PersistItemsJob.class); - - private final PersistenceManagerImpl manager; - private final String dbId; - private final String strategyName; - - public PersistItemsJob(final PersistenceManagerImpl manager, final String dbId, final String strategyName) { - this.manager = manager; - this.dbId = dbId; - this.strategyName = strategyName; - } - - @Override - public void run() { - synchronized (manager.persistenceServiceConfigs) { - final PersistenceService persistenceService = manager.persistenceServices.get(dbId); - final PersistenceServiceConfiguration config = manager.persistenceServiceConfigs.get(dbId); - - if (persistenceService != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config.getDefaults(), itemConfig, strategyName)) { - for (Item item : manager.getAllItems(itemConfig)) { - long startTime = System.nanoTime(); - persistenceService.store(item, itemConfig.getAlias()); - logger.trace("Storing item '{}' with persistence service '{}' took {}ms", item.getName(), - dbId, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); - } - } - - } - } - } - } - - private boolean hasStrategy(List defaults, PersistenceItemConfiguration config, - String strategyName) { - // check if the strategy is directly defined on the config - for (PersistenceStrategy strategy : config.getStrategies()) { - if (strategyName.equals(strategy.getName())) { - return true; - } - } - // if no strategies are given, check the default strategies to use - return config.getStrategies().isEmpty() && isDefault(defaults, strategyName); - } - - private boolean isDefault(List defaults, String strategyName) { - for (PersistenceStrategy strategy : defaults) { - if (strategy.getName().equals(strategyName)) { - return true; - } - } - return false; - } -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java new file mode 100644 index 00000000000..69b7fae7c66 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManager.java @@ -0,0 +1,465 @@ +/** + * Copyright (c) 2010-2023 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.internal; + +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.common.SafeCaller; +import org.openhab.core.items.GenericItem; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.ItemRegistryChangeListener; +import org.openhab.core.items.StateChangeListener; +import org.openhab.core.persistence.FilterCriteria; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.QueryablePersistenceService; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistryChangeListener; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyMarkerFilter; +import org.openhab.core.service.ReadyService; +import org.openhab.core.service.ReadyService.ReadyTracker; +import org.openhab.core.service.StartLevelService; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements a persistence manager to manage all persistence services etc. + * + * @author Kai Kreuzer - Initial contribution + * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. + * @author Jan N. Klug - Refactored to use service configuration registry + */ +@Component(immediate = true) +@NonNullByDefault +public class PersistenceManager implements ItemRegistryChangeListener, StateChangeListener, ReadyTracker, + PersistenceServiceConfigurationRegistryChangeListener { + + private final Logger logger = LoggerFactory.getLogger(PersistenceManager.class); + + private final ReadyMarker marker = new ReadyMarker("persistence", "restore"); + + // the scheduler used for timer events + private final CronScheduler scheduler; + private final ItemRegistry itemRegistry; + private final SafeCaller safeCaller; + private final ReadyService readyService; + private final PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry; + + private volatile boolean started = false; + + private final Map persistenceServiceContainers = new ConcurrentHashMap<>(); + + @Activate + public PersistenceManager(final @Reference CronScheduler scheduler, final @Reference ItemRegistry itemRegistry, + final @Reference SafeCaller safeCaller, final @Reference ReadyService readyService, + final @Reference PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistry) { + this.scheduler = scheduler; + this.itemRegistry = itemRegistry; + this.safeCaller = safeCaller; + this.readyService = readyService; + this.persistenceServiceConfigurationRegistry = persistenceServiceConfigurationRegistry; + + persistenceServiceConfigurationRegistry.addRegistryChangeListener(this); + readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) + .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); + } + + @Deactivate + protected void deactivate() { + itemRegistry.removeRegistryChangeListener(this); + persistenceServiceConfigurationRegistry.removeRegistryChangeListener(this); + started = false; + + persistenceServiceContainers.values().forEach(PersistenceServiceContainer::cancelPersistJobs); + + // remove item state change listeners + itemRegistry.stream().filter(GenericItem.class::isInstance) + .forEach(item -> ((GenericItem) item).removeStateChangeListener(this)); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addPersistenceService(PersistenceService persistenceService) { + String serviceId = persistenceService.getId(); + logger.debug("Initializing {} persistence service.", serviceId); + PersistenceServiceContainer container = new PersistenceServiceContainer(persistenceService, + persistenceServiceConfigurationRegistry.get(serviceId)); + + PersistenceServiceContainer oldContainer = persistenceServiceContainers.put(serviceId, container); + + if (oldContainer != null) { // cancel all jobs if the persistence service is set and an old configuration is + // already present + oldContainer.cancelPersistJobs(); + } + + if (started) { + startEventHandling(container); + } + } + + protected void removePersistenceService(PersistenceService persistenceService) { + PersistenceServiceContainer container = persistenceServiceContainers.remove(persistenceService.getId()); + if (container != null) { + container.cancelPersistJobs(); + } + } + + /** + * Calls all persistence services which use change or update policy for the given item + * + * @param item the item to persist + * @param changed true, if it has the change strategy, false otherwise + */ + private void handleStateEvent(Item item, boolean changed) { + PersistenceStrategy changeStrategy = changed ? PersistenceStrategy.Globals.CHANGE + : PersistenceStrategy.Globals.UPDATE; + + persistenceServiceContainers.values() + .forEach(container -> container.getMatchingConfigurations(changeStrategy) + .filter(itemConfig -> appliesToItem(itemConfig, item)) + .filter(itemConfig -> itemConfig.filters().stream().allMatch(filter -> filter.apply(item))) + .forEach(itemConfig -> { + itemConfig.filters().forEach(filter -> filter.persisted(item)); + container.getPersistenceService().store(item, itemConfig.alias()); + })); + } + + /** + * Checks if a given persistence configuration entry is relevant for an item + * + * @param itemConfig the persistence configuration entry + * @param item to check if the configuration applies to + * @return true, if the configuration applies to the item + */ + private boolean appliesToItem(PersistenceItemConfiguration itemConfig, Item item) { + for (PersistenceConfig itemCfg : itemConfig.items()) { + if (itemCfg instanceof PersistenceAllConfig) { + return true; + } else if (itemCfg instanceof PersistenceItemConfig) { + if (item.getName().equals(((PersistenceItemConfig) itemCfg).getItem())) { + return true; + } + } else if (itemCfg instanceof PersistenceGroupConfig) { + try { + Item gItem = itemRegistry.getItem(((PersistenceGroupConfig) itemCfg).getGroup()); + if (gItem instanceof GroupItem) { + return ((GroupItem) gItem).getAllMembers().contains(item); + } + } catch (ItemNotFoundException e) { + // do nothing + } + } + } + return false; + } + + /** + * Retrieves all items for which the persistence configuration applies to. + * + * @param config the persistence configuration entry + * @return all items that this configuration applies to + */ + private Iterable getAllItems(PersistenceItemConfiguration config) { + // first check, if we should return them all + if (config.items().stream().anyMatch(PersistenceAllConfig.class::isInstance)) { + return itemRegistry.getItems(); + } + + // otherwise, go through the detailed definitions + Set items = new HashSet<>(); + for (Object itemCfg : config.items()) { + if (itemCfg instanceof PersistenceItemConfig) { + String itemName = ((PersistenceItemConfig) itemCfg).getItem(); + try { + items.add(itemRegistry.getItem(itemName)); + } catch (ItemNotFoundException e) { + logger.debug("Item '{}' does not exist.", itemName); + } + } + if (itemCfg instanceof PersistenceGroupConfig) { + String groupName = ((PersistenceGroupConfig) itemCfg).getGroup(); + try { + Item gItem = itemRegistry.getItem(groupName); + if (gItem instanceof GroupItem groupItem) { + items.addAll(groupItem.getAllMembers()); + } + } catch (ItemNotFoundException e) { + logger.debug("Item group '{}' does not exist.", groupName); + } + } + } + return items; + } + + /** + * Handles the "restoreOnStartup" strategy for the item. + * If the item state is still undefined when entering this method, all persistence configurations are checked, + * if they have the "restoreOnStartup" strategy configured for the item. If so, the item state will be set + * to its last persisted value. + * + * @param item the item to restore the state for + */ + private void restoreItemStateIfNeeded(Item item) { + // get the last persisted state from the persistence service if no state is yet set + if (UnDefType.NULL.equals(item.getState()) && item instanceof GenericItem) { + List matchingContainers = persistenceServiceContainers.values().stream() // + .filter(container -> container.getPersistenceService() instanceof QueryablePersistenceService) // + .filter(container -> container.getMatchingConfigurations(PersistenceStrategy.Globals.RESTORE) + .anyMatch(itemConfig -> appliesToItem(itemConfig, item))) + .toList(); + + for (PersistenceServiceContainer container : matchingContainers) { + QueryablePersistenceService queryService = (QueryablePersistenceService) container + .getPersistenceService(); + FilterCriteria filter = new FilterCriteria().setItemName(item.getName()).setPageSize(1); + Iterator result = safeCaller.create(queryService, QueryablePersistenceService.class) + .onTimeout(() -> logger.warn("Querying persistence service '{}' takes more than {}ms.", + queryService.getId(), SafeCaller.DEFAULT_TIMEOUT)) + .onException(e -> logger.error("Exception occurred while querying persistence service '{}': {}", + queryService.getId(), e.getMessage(), e)) + .build().query(filter).iterator(); + if (result.hasNext()) { + HistoricItem historicItem = result.next(); + GenericItem genericItem = (GenericItem) item; + genericItem.removeStateChangeListener(this); + genericItem.setState(historicItem.getState()); + genericItem.addStateChangeListener(this); + if (logger.isDebugEnabled()) { + logger.debug("Restored item state from '{}' for item '{}' -> '{}'", + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(historicItem.getTimestamp()), + item.getName(), historicItem.getState()); + } + return; + } + } + } + } + + private void startEventHandling(PersistenceServiceContainer serviceContainer) { + serviceContainer.getMatchingConfigurations(PersistenceStrategy.Globals.RESTORE) + .forEach(itemConfig -> getAllItems(itemConfig).forEach(this::restoreItemStateIfNeeded)); + serviceContainer.schedulePersistJobs(); + } + + // ItemStateChangeListener methods + + @Override + public void allItemsChanged(Collection oldItemNames) { + itemRegistry.getItems().forEach(this::added); + } + + @Override + public void added(Item item) { + restoreItemStateIfNeeded(item); + if (item instanceof GenericItem genericItem) { + genericItem.addStateChangeListener(this); + } + } + + @Override + public void removed(Item item) { + if (item instanceof GenericItem genericItem) { + genericItem.removeStateChangeListener(this); + } + } + + @Override + public void updated(Item oldItem, Item item) { + removed(oldItem); + added(item); + } + + @Override + public void stateChanged(Item item, State oldState, State newState) { + handleStateEvent(item, true); + } + + @Override + public void stateUpdated(Item item, State state) { + handleStateEvent(item, false); + } + + @Override + public void onReadyMarkerAdded(ReadyMarker readyMarker) { + ExecutorService scheduler = Executors.newSingleThreadExecutor(new NamedThreadFactory("persistenceManager")); + scheduler.submit(() -> { + allItemsChanged(Set.of()); + persistenceServiceContainers.values().forEach(this::startEventHandling); + started = true; + readyService.markReady(marker); + itemRegistry.addRegistryChangeListener(this); + }); + scheduler.shutdown(); + } + + @Override + public void onReadyMarkerRemoved(ReadyMarker readyMarker) { + readyService.unmarkReady(marker); + } + + @Override + public void added(PersistenceServiceConfiguration element) { + PersistenceServiceContainer container = persistenceServiceContainers.get(element.getUID()); + if (container != null) { + container.setConfiguration(element); + if (started) { + startEventHandling(container); + } + } + } + + @Override + public void removed(PersistenceServiceConfiguration element) { + PersistenceServiceContainer container = persistenceServiceContainers.get(element.getUID()); + if (container != null) { + container.setConfiguration(null); + if (started) { + startEventHandling(container); + } + } + } + + @Override + public void updated(PersistenceServiceConfiguration oldElement, PersistenceServiceConfiguration element) { + // no need to remove before, configuration is overwritten if possible + added(element); + } + + private class PersistenceServiceContainer { + private final PersistenceService persistenceService; + private final Set> jobs = new HashSet<>(); + + private PersistenceServiceConfiguration configuration; + + public PersistenceServiceContainer(PersistenceService persistenceService, + @Nullable PersistenceServiceConfiguration configuration) { + this.persistenceService = persistenceService; + this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); + } + + public PersistenceService getPersistenceService() { + return persistenceService; + } + + /** + * Set a new configuration for this persistence service (also cancels all cron jobs) + * + * @param configuration the new {@link PersistenceServiceConfiguration}, if {@code null} the default + * configuration of the service is used + */ + public void setConfiguration(@Nullable PersistenceServiceConfiguration configuration) { + cancelPersistJobs(); + this.configuration = Objects.requireNonNullElseGet(configuration, this::getDefaultConfig); + } + + /** + * Get all item configurations from this service that match a certain strategy + * + * @param strategy the {@link PersistenceStrategy} to look for + * @return a @link Stream} of the result + */ + public Stream getMatchingConfigurations(PersistenceStrategy strategy) { + boolean matchesDefaultStrategies = configuration.getDefaults().contains(strategy); + return configuration.getConfigs().stream().filter(itemConfig -> itemConfig.strategies().contains(strategy) + || (itemConfig.strategies().isEmpty() && matchesDefaultStrategies)); + } + + private PersistenceServiceConfiguration getDefaultConfig() { + List strategies = persistenceService.getDefaultStrategies(); + List configs = List + .of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null)); + return new PersistenceServiceConfiguration(persistenceService.getId(), configs, strategies, strategies, + List.of()); + } + + /** + * Cancel all scheduled cron jobs / strategies for this service + */ + public void cancelPersistJobs() { + synchronized (jobs) { + jobs.forEach(job -> job.cancel(true)); + jobs.clear(); + } + logger.debug("Removed scheduled cron job for persistence service '{}'", configuration.getUID()); + } + + /** + * Schedule all necessary cron jobs / strategies for this service + */ + public void schedulePersistJobs() { + configuration.getStrategies().stream().filter(PersistenceCronStrategy.class::isInstance) + .forEach(strategy -> { + PersistenceCronStrategy cronStrategy = (PersistenceCronStrategy) strategy; + String cronExpression = cronStrategy.getCronExpression(); + List itemConfigs = getMatchingConfigurations(strategy) + .collect(Collectors.toList()); + jobs.add(scheduler.schedule(() -> persistJob(itemConfigs), cronExpression)); + + logger.debug("Scheduled strategy {} with cron expression {} for service {}", + cronStrategy.getName(), cronExpression, configuration.getUID()); + + }); + } + + private void persistJob(List itemConfigs) { + itemConfigs.forEach(itemConfig -> { + for (Item item : getAllItems(itemConfig)) { + if (itemConfig.filters().stream().allMatch(filter -> filter.apply(item))) { + long startTime = System.nanoTime(); + itemConfig.filters().forEach(filter -> filter.persisted(item)); + persistenceService.store(item, itemConfig.alias()); + logger.trace("Storing item '{}' with persistence service '{}' took {}ms", item.getName(), + configuration.getUID(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); + } + } + }); + } + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java deleted file mode 100644 index be9dcf0e5bb..00000000000 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceManagerImpl.java +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.internal; - -import java.time.format.DateTimeFormatter; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.common.NamedThreadFactory; -import org.openhab.core.common.SafeCaller; -import org.openhab.core.items.GenericItem; -import org.openhab.core.items.GroupItem; -import org.openhab.core.items.Item; -import org.openhab.core.items.ItemNotFoundException; -import org.openhab.core.items.ItemRegistry; -import org.openhab.core.items.ItemRegistryChangeListener; -import org.openhab.core.items.StateChangeListener; -import org.openhab.core.persistence.FilterCriteria; -import org.openhab.core.persistence.HistoricItem; -import org.openhab.core.persistence.PersistenceItemConfiguration; -import org.openhab.core.persistence.PersistenceManager; -import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceConfiguration; -import org.openhab.core.persistence.QueryablePersistenceService; -import org.openhab.core.persistence.config.PersistenceAllConfig; -import org.openhab.core.persistence.config.PersistenceConfig; -import org.openhab.core.persistence.config.PersistenceGroupConfig; -import org.openhab.core.persistence.config.PersistenceItemConfig; -import org.openhab.core.persistence.strategy.PersistenceCronStrategy; -import org.openhab.core.persistence.strategy.PersistenceStrategy; -import org.openhab.core.scheduler.CronScheduler; -import org.openhab.core.scheduler.ScheduledCompletableFuture; -import org.openhab.core.service.ReadyMarker; -import org.openhab.core.service.ReadyMarkerFilter; -import org.openhab.core.service.ReadyService; -import org.openhab.core.service.ReadyService.ReadyTracker; -import org.openhab.core.service.StartLevelService; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class implements a persistence manager to manage all persistence services etc. - * - * @author Kai Kreuzer - Initial contribution - * @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage. - */ -@Component(immediate = true, service = PersistenceManager.class) -@NonNullByDefault -public class PersistenceManagerImpl - implements ItemRegistryChangeListener, PersistenceManager, StateChangeListener, ReadyTracker { - - private final Logger logger = LoggerFactory.getLogger(PersistenceManagerImpl.class); - - private final ReadyMarker marker = new ReadyMarker("persistence", "restore"); - - // the scheduler used for timer events - private final CronScheduler scheduler; - private final ItemRegistry itemRegistry; - private final SafeCaller safeCaller; - private final ReadyService readyService; - private volatile boolean started = false; - - final Map persistenceServices = new HashMap<>(); - final Map persistenceServiceConfigs = new HashMap<>(); - private final Map>> persistenceJobs = new HashMap<>(); - - @Activate - public PersistenceManagerImpl(final @Reference CronScheduler scheduler, final @Reference ItemRegistry itemRegistry, - final @Reference SafeCaller safeCaller, final @Reference ReadyService readyService) { - this.scheduler = scheduler; - this.itemRegistry = itemRegistry; - this.safeCaller = safeCaller; - this.readyService = readyService; - } - - @Activate - protected void activate() { - readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) - .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); - } - - @Deactivate - protected void deactivate() { - itemRegistry.removeRegistryChangeListener(this); - started = false; - removeTimers(); - removeItemStateChangeListeners(); - } - - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - protected void addPersistenceService(PersistenceService persistenceService) { - logger.debug("Initializing {} persistence service.", persistenceService.getId()); - persistenceServices.put(persistenceService.getId(), persistenceService); - persistenceServiceConfigs.putIfAbsent(persistenceService.getId(), getDefaultConfig(persistenceService)); - if (started) { - stopEventHandling(persistenceService.getId()); - startEventHandling(persistenceService.getId()); - } - } - - protected void removePersistenceService(PersistenceService persistenceService) { - stopEventHandling(persistenceService.getId()); - persistenceServices.remove(persistenceService.getId()); - } - - /** - * Calls all persistence services which use change or update policy for the given item - * - * @param item the item to persist - * @param onlyChanges true, if it has the change strategy, false otherwise - */ - private void handleStateEvent(Item item, boolean onlyChanges) { - synchronized (persistenceServiceConfigs) { - for (Entry entry : persistenceServiceConfigs - .entrySet()) { - final String serviceName = entry.getKey(); - final PersistenceServiceConfiguration config = entry.getValue(); - if (config != null && persistenceServices.containsKey(serviceName)) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, onlyChanges ? PersistenceStrategy.Globals.CHANGE - : PersistenceStrategy.Globals.UPDATE)) { - if (appliesToItem(itemConfig, item)) { - persistenceServices.get(serviceName).store(item, itemConfig.getAlias()); - } - } - } - } - } - } - } - - /** - * Checks if a given persistence configuration entry has a certain strategy for the given service - * - * @param config the configuration to check for - * @param itemConfig the persistence configuration entry - * @param strategy the strategy to check for - * @return true, if it has the given strategy - */ - private boolean hasStrategy(PersistenceServiceConfiguration config, PersistenceItemConfiguration itemConfig, - PersistenceStrategy strategy) { - if (config.getDefaults().contains(strategy) && itemConfig.getStrategies().isEmpty()) { - return true; - } else { - for (PersistenceStrategy s : itemConfig.getStrategies()) { - if (s.equals(strategy)) { - return true; - } - } - return false; - } - } - - /** - * Checks if a given persistence configuration entry is relevant for an item - * - * @param config the persistence configuration entry - * @param item to check if the configuration applies to - * @return true, if the configuration applies to the item - */ - private boolean appliesToItem(PersistenceItemConfiguration config, Item item) { - for (PersistenceConfig itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceAllConfig) { - return true; - } - if (itemCfg instanceof PersistenceItemConfig singleItemConfig) { - if (item.getName().equals(singleItemConfig.getItem())) { - return true; - } - } - if (itemCfg instanceof PersistenceGroupConfig groupItemConfig) { - try { - Item gItem = itemRegistry.getItem(groupItemConfig.getGroup()); - if (gItem instanceof GroupItem groupItem) { - if (groupItem.getAllMembers().contains(item)) { - return true; - } - } - } catch (ItemNotFoundException e) { - // do nothing - } - } - } - return false; - } - - /** - * Retrieves all items for which the persistence configuration applies to. - * - * @param config the persistence configuration entry - * @return all items that this configuration applies to - */ - Iterable getAllItems(PersistenceItemConfiguration config) { - // first check, if we should return them all - for (Object itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceAllConfig) { - return itemRegistry.getItems(); - } - } - - // otherwise, go through the detailed definitions - Set items = new HashSet<>(); - for (Object itemCfg : config.getItems()) { - if (itemCfg instanceof PersistenceItemConfig singleItemConfig) { - String itemName = singleItemConfig.getItem(); - try { - items.add(itemRegistry.getItem(itemName)); - } catch (ItemNotFoundException e) { - logger.debug("Item '{}' does not exist.", itemName); - } - } - if (itemCfg instanceof PersistenceGroupConfig groupItemConfig) { - String groupName = groupItemConfig.getGroup(); - try { - Item gItem = itemRegistry.getItem(groupName); - if (gItem instanceof GroupItem groupItem) { - items.addAll(groupItem.getAllMembers()); - } - } catch (ItemNotFoundException e) { - logger.debug("Item group '{}' does not exist.", groupName); - } - } - } - return items; - } - - /** - * Handles the "restoreOnStartup" strategy for the item. - * If the item state is still undefined when entering this method, all persistence configurations are checked, - * if they have the "restoreOnStartup" strategy configured for the item. If so, the item state will be set - * to its last persisted value. - * - * @param item the item to restore the state for - */ - @SuppressWarnings("null") - private void initialize(Item item) { - // get the last persisted state from the persistence service if no state is yet set - if (UnDefType.NULL.equals(item.getState()) && item instanceof GenericItem genericItem) { - for (Entry entry : persistenceServiceConfigs - .entrySet()) { - final String serviceName = entry.getKey(); - final PersistenceServiceConfiguration config = entry.getValue(); - if (config != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, PersistenceStrategy.Globals.RESTORE)) { - if (appliesToItem(itemConfig, item)) { - PersistenceService service = persistenceServices.get(serviceName); - if (service instanceof QueryablePersistenceService queryService) { - FilterCriteria filter = new FilterCriteria().setItemName(item.getName()) - .setPageSize(1); - Iterable result = safeCaller - .create(queryService, QueryablePersistenceService.class).onTimeout(() -> { - logger.warn("Querying persistence service '{}' takes more than {}ms.", - queryService.getId(), SafeCaller.DEFAULT_TIMEOUT); - }).onException(e -> { - logger.error( - "Exception occurred while querying persistence service '{}': {}", - queryService.getId(), e.getMessage(), e); - }).build().query(filter); - if (result != null) { - Iterator it = result.iterator(); - if (it.hasNext()) { - HistoricItem historicItem = it.next(); - genericItem.removeStateChangeListener(this); - genericItem.setState(historicItem.getState()); - genericItem.addStateChangeListener(this); - if (logger.isDebugEnabled()) { - logger.debug("Restored item state from '{}' for item '{}' -> '{}'", - DateTimeFormatter.ISO_ZONED_DATE_TIME - .format(historicItem.getTimestamp()), - item.getName(), historicItem.getState()); - } - return; - } - } - } else if (service != null) { - logger.warn( - "Failed to restore item states as persistence service '{}' cannot be queried.", - serviceName); - } - } - } - } - } - } - } - } - - private void removeItemStateChangeListeners() { - for (Item item : itemRegistry.getAll()) { - if (item instanceof GenericItem genericItem) { - genericItem.removeStateChangeListener(this); - } - } - } - - /** - * Creates new {@link ScheduledCompletableFuture}s in the group dbId for the given collection of - * {@link PersistenceStrategy strategies}. - * - * @param dbId the database id used by the persistence service - * @param strategies a collection of strategies - */ - private void createTimers(final String dbId, List strategies) { - for (PersistenceStrategy strategy : strategies) { - if (strategy instanceof PersistenceCronStrategy cronStrategy) { - String cronExpression = cronStrategy.getCronExpression(); - - final PersistItemsJob job = new PersistItemsJob(this, dbId, cronStrategy.getName()); - ScheduledCompletableFuture schedule = scheduler.schedule(job, cronExpression); - if (persistenceJobs.containsKey(dbId)) { - persistenceJobs.get(dbId).add(schedule); - } else { - final Set> jobs = new HashSet<>(); - jobs.add(schedule); - persistenceJobs.put(dbId, jobs); - } - - logger.debug("Scheduled strategy {} with cron expression {}", cronStrategy.getName(), cronExpression); - } - } - } - - /** - * Deletes all {@link ScheduledCompletableFuture}s of the group dbId. - * - * @param dbId the database id used by the persistence service - */ - private void removeTimers(String dbId) { - if (!persistenceJobs.containsKey(dbId)) { - return; - } - for (final ScheduledCompletableFuture job : persistenceJobs.get(dbId)) { - job.cancel(true); - logger.debug("Removed scheduled cron job for persistence service '{}'", dbId); - } - persistenceJobs.remove(dbId); - } - - private void removeTimers() { - Set dbIds = new HashSet<>(persistenceJobs.keySet()); - for (String dbId : dbIds) { - removeTimers(dbId); - } - } - - @Override - public void addConfig(final String dbId, final PersistenceServiceConfiguration config) { - synchronized (persistenceServiceConfigs) { - if (started && persistenceServiceConfigs.containsKey(dbId)) { - stopEventHandling(dbId); - } - persistenceServiceConfigs.put(dbId, config); - if (started && persistenceServices.containsKey(dbId)) { - startEventHandling(dbId); - } - } - } - - @Override - public void removeConfig(final String dbId) { - synchronized (persistenceServiceConfigs) { - stopEventHandling(dbId); - PersistenceService persistenceService = persistenceServices.get(dbId); - if (persistenceService != null) { - persistenceServiceConfigs.put(dbId, getDefaultConfig(persistenceService)); - startEventHandling(dbId); - } else { - persistenceServiceConfigs.remove(dbId); - } - } - } - - private void startEventHandling(final String serviceName) { - synchronized (persistenceServiceConfigs) { - final PersistenceServiceConfiguration config = persistenceServiceConfigs.get(serviceName); - if (config != null) { - for (PersistenceItemConfiguration itemConfig : config.getConfigs()) { - if (hasStrategy(config, itemConfig, PersistenceStrategy.Globals.RESTORE)) { - for (Item item : getAllItems(itemConfig)) { - initialize(item); - } - } - } - createTimers(serviceName, config.getStrategies()); - } - } - } - - private void stopEventHandling(String dbId) { - synchronized (persistenceServiceConfigs) { - removeTimers(dbId); - } - } - - private @Nullable PersistenceServiceConfiguration getDefaultConfig(PersistenceService persistenceService) { - List strategies = persistenceService.getDefaultStrategies(); - List configs = List - .of(new PersistenceItemConfiguration(List.of(new PersistenceAllConfig()), null, strategies, null)); - return new PersistenceServiceConfiguration(configs, strategies, strategies); - } - - @Override - public void allItemsChanged(Collection oldItemNames) { - for (Item item : itemRegistry.getItems()) { - added(item); - } - } - - @Override - public void added(Item item) { - initialize(item); - if (item instanceof GenericItem genericItem) { - genericItem.addStateChangeListener(this); - } - } - - @Override - public void removed(Item item) { - if (item instanceof GenericItem genericItem) { - genericItem.removeStateChangeListener(this); - } - } - - @Override - public void updated(Item oldItem, Item item) { - removed(oldItem); - added(item); - } - - @Override - public void stateChanged(Item item, State oldState, State newState) { - handleStateEvent(item, true); - } - - @Override - public void stateUpdated(Item item, State state) { - handleStateEvent(item, false); - } - - @Override - public void onReadyMarkerAdded(ReadyMarker readyMarker) { - ExecutorService scheduler = Executors.newSingleThreadExecutor(new NamedThreadFactory("persistenceManager")); - scheduler.submit(() -> { - allItemsChanged(Collections.emptySet()); - for (String dbId : persistenceServices.keySet()) { - startEventHandling(dbId); - } - started = true; - readyService.markReady(marker); - itemRegistry.addRegistryChangeListener(this); - }); - scheduler.shutdown(); - } - - @Override - public void onReadyMarkerRemoved(ReadyMarker readyMarker) { - readyService.unmarkReady(marker); - } -} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java new file mode 100644 index 00000000000..69e4cc151fa --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/internal/PersistenceServiceConfigurationRegistryImpl.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2023 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.internal; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.common.registry.Provider; +import org.openhab.core.persistence.registry.ManagedPersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationProvider; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistryChangeListener; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PersistenceServiceConfigurationRegistryImpl} implements the + * {@link PersistenceServiceConfigurationRegistry} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = PersistenceServiceConfigurationRegistry.class) +public class PersistenceServiceConfigurationRegistryImpl + extends AbstractRegistry + implements PersistenceServiceConfigurationRegistry { + private final Logger logger = LoggerFactory.getLogger(PersistenceServiceConfigurationRegistryImpl.class); + private final Map> serviceToProvider = new HashMap<>(); + private final Set registryChangeListeners = new CopyOnWriteArraySet<>(); + + public PersistenceServiceConfigurationRegistryImpl() { + super(PersistenceServiceConfigurationProvider.class); + } + + @Override + public void added(Provider provider, PersistenceServiceConfiguration element) { + if (serviceToProvider.containsKey(element.getUID())) { + logger.warn("Tried to add strategy container with serviceId '{}', but it was already added before.", + element.getUID()); + } else { + super.added(provider, element); + } + } + + @Override + public void removed(Provider provider, PersistenceServiceConfiguration element) { + if (!provider.equals(serviceToProvider.getOrDefault(element.getUID(), provider))) { + logger.warn("Tried to remove strategy container with serviceId '{}', but it was added by another provider.", + element.getUID()); + } else { + super.removed(provider, element); + } + } + + @Override + public void updated(Provider provider, PersistenceServiceConfiguration oldelement, + PersistenceServiceConfiguration element) { + if (!provider.equals(serviceToProvider.getOrDefault(element.getUID(), provider))) { + logger.warn("Tried to update strategy container with serviceId '{}', but it was added by another provider.", + element.getUID()); + } else { + super.updated(provider, oldelement, element); + } + } + + protected void notifyListenersAboutAddedElement(PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.added(element)); + super.notifyListenersAboutAddedElement(element); + } + + protected void notifyListenersAboutRemovedElement(PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.removed(element)); + super.notifyListenersAboutRemovedElement(element); + } + + protected void notifyListenersAboutUpdatedElement(PersistenceServiceConfiguration oldElement, + PersistenceServiceConfiguration element) { + registryChangeListeners.forEach(listener -> listener.updated(oldElement, element)); + } + + @Override + public void addRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener) { + registryChangeListeners.add(listener); + } + + @Override + public void removeRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener) { + registryChangeListeners.remove(listener); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setManagedProvider(ManagedPersistenceServiceConfigurationProvider provider) { + super.setManagedProvider(provider); + } + + protected void unsetManagedProvider(ManagedPersistenceServiceConfigurationProvider provider) { + super.unsetManagedProvider(provider); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java new file mode 100644 index 00000000000..4bbd84d9626 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/ManagedPersistenceServiceConfigurationProvider.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2023 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.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractManagedProvider; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; +import org.openhab.core.storage.StorageService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link ManagedPersistenceServiceConfigurationProvider} implements a + * {@link PersistenceServiceConfigurationProvider} for managed configurations which are stored in a JSON database + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { PersistenceServiceConfigurationProvider.class, + ManagedPersistenceServiceConfigurationProvider.class }) +public class ManagedPersistenceServiceConfigurationProvider + extends AbstractManagedProvider + implements PersistenceServiceConfigurationProvider { + private static final String STORAGE_NAME = "org.openhab.core.persistence.PersistenceServiceConfiguration"; + + @Activate + public ManagedPersistenceServiceConfigurationProvider(@Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return STORAGE_NAME; + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected @Nullable PersistenceServiceConfiguration toElement(String key, + PersistenceServiceConfigurationDTO persistableElement) { + return PersistenceServiceConfigurationDTOMapper.map(persistableElement); + } + + @Override + protected PersistenceServiceConfigurationDTO toPersistableElement(PersistenceServiceConfiguration element) { + return PersistenceServiceConfigurationDTOMapper.map(element); + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java similarity index 51% rename from bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java rename to bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java index b31930ad896..214d584a83d 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/PersistenceServiceConfiguration.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfiguration.java @@ -10,32 +10,43 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.persistence; +package org.openhab.core.persistence.registry; import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Identifiable; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.filter.PersistenceFilter; import org.openhab.core.persistence.strategy.PersistenceStrategy; /** - * This class represents the configuration for a persistence service. + * The {@link PersistenceServiceConfiguration} represents the configuration for a persistence service. * - * @author Markus Rathgeb - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class PersistenceServiceConfiguration { +public class PersistenceServiceConfiguration implements Identifiable { + private final String serviceId; private final List configs; private final List defaults; private final List strategies; + private final List filters; - public PersistenceServiceConfiguration(final Collection configs, - final Collection defaults, final Collection strategies) { - this.configs = Collections.unmodifiableList(new LinkedList<>(configs)); - this.defaults = Collections.unmodifiableList(new LinkedList<>(defaults)); - this.strategies = Collections.unmodifiableList(new LinkedList<>(strategies)); + public PersistenceServiceConfiguration(String serviceId, Collection configs, + Collection defaults, Collection strategies, + Collection filters) { + this.serviceId = serviceId; + this.configs = List.copyOf(configs); + this.defaults = List.copyOf(defaults); + this.strategies = List.copyOf(strategies); + this.filters = List.copyOf(filters); + } + + @Override + public String getUID() { + return serviceId; } /** @@ -64,4 +75,13 @@ public List getDefaults() { public List getStrategies() { return strategies; } + + /** + * Get all defined filters. + * + * @return an unmodifiable list of the defined filters + */ + public List getFilters() { + return filters; + } } diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java new file mode 100644 index 00000000000..2b9b513ed18 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationDTOMapper.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2023 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.registry; + +import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.STRATEGIES; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.dto.PersistenceCronStrategyDTO; +import org.openhab.core.persistence.dto.PersistenceFilterDTO; +import org.openhab.core.persistence.dto.PersistenceItemConfigurationDTO; +import org.openhab.core.persistence.dto.PersistenceServiceConfigurationDTO; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.filter.PersistenceTimeFilter; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; + +/** + * The {@link PersistenceServiceConfigurationDTOMapper} is a utility class to map persistence configurations for storage + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceServiceConfigurationDTOMapper { + + private PersistenceServiceConfigurationDTOMapper() { + // prevent initialization + } + + public static PersistenceServiceConfigurationDTO map( + PersistenceServiceConfiguration persistenceServiceConfiguration) { + PersistenceServiceConfigurationDTO dto = new PersistenceServiceConfigurationDTO(); + dto.serviceId = persistenceServiceConfiguration.getUID(); + dto.configs = persistenceServiceConfiguration.getConfigs().stream() + .map(PersistenceServiceConfigurationDTOMapper::mapPersistenceItemConfig).toList(); + dto.defaults = persistenceServiceConfiguration.getDefaults().stream().map(PersistenceStrategy::getName) + .toList(); + dto.cronStrategies = filterList(persistenceServiceConfiguration.getStrategies(), PersistenceCronStrategy.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceCronStrategy); + dto.thresholdFilters = filterList(persistenceServiceConfiguration.getFilters(), + PersistenceThresholdFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceThresholdFilter); + dto.timeFilters = filterList(persistenceServiceConfiguration.getFilters(), PersistenceTimeFilter.class, + PersistenceServiceConfigurationDTOMapper::mapPersistenceTimeFilter); + + return dto; + } + + public static PersistenceServiceConfiguration map(PersistenceServiceConfigurationDTO dto) { + Map strategyMap = dto.cronStrategies.stream() + .collect(Collectors.toMap(e -> e.name, e -> new PersistenceCronStrategy(e.name, e.cronExpression))); + + Map filterMap = Stream + .concat(dto.thresholdFilters.stream().map(f -> new PersistenceThresholdFilter(f.name, f.value, f.unit)), + dto.timeFilters.stream() + .map(f -> new PersistenceTimeFilter(f.name, f.value.intValue(), f.unit))) + .collect(Collectors.toMap(PersistenceFilter::getName, e -> e)); + + List defaults = dto.defaults.stream() + .map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList(); + + List configs = dto.configs.stream().map(config -> { + List items = config.items.stream() + .map(PersistenceServiceConfigurationDTOMapper::stringToPersistenceConfig).toList(); + List strategies = config.strategies.stream() + .map(str -> stringToPersistenceStrategy(str, strategyMap, dto.serviceId)).toList(); + return new PersistenceItemConfiguration(items, config.alias, strategies, List.of()); + }).toList(); + + return new PersistenceServiceConfiguration(dto.serviceId, configs, defaults, strategyMap.values(), + filterMap.values()); + } + + private static Collection filterList(Collection list, Class clazz, Function mapper) { + return list.stream().filter(clazz::isInstance).map(clazz::cast).map(mapper).toList(); + } + + private static PersistenceConfig stringToPersistenceConfig(String string) { + if ("*".equals(string)) { + return new PersistenceAllConfig(); + } else if (string.endsWith("*")) { + return new PersistenceGroupConfig(string.substring(0, string.length() - 1)); + } else { + return new PersistenceItemConfig(string); + } + } + + private static PersistenceStrategy stringToPersistenceStrategy(String string, + Map strategyMap, String serviceId) { + PersistenceStrategy strategy = strategyMap.get(string); + if (strategy != null) { + return strategy; + } + strategy = STRATEGIES.get(string); + if (strategy != null) { + return strategy; + } + throw new IllegalArgumentException("Strategy '" + string + "' unknown for service '" + serviceId + "'"); + } + + private static String persistenceConfigToString(PersistenceConfig config) { + if (config instanceof PersistenceAllConfig) { + return "*"; + } else if (config instanceof PersistenceGroupConfig persistenceGroupConfig) { + return persistenceGroupConfig.getGroup() + "*"; + } else if (config instanceof PersistenceItemConfig persistenceItemConfig) { + return persistenceItemConfig.getItem(); + } + throw new IllegalArgumentException("Unknown persistence config class " + config.getClass()); + } + + private static PersistenceItemConfigurationDTO mapPersistenceItemConfig(PersistenceItemConfiguration config) { + PersistenceItemConfigurationDTO itemDto = new PersistenceItemConfigurationDTO(); + itemDto.items = config.items().stream().map(PersistenceServiceConfigurationDTOMapper::persistenceConfigToString) + .toList(); + itemDto.strategies = config.strategies().stream().map(PersistenceStrategy::getName).toList(); + itemDto.alias = config.alias(); + return itemDto; + } + + private static PersistenceCronStrategyDTO mapPersistenceCronStrategy(PersistenceCronStrategy cronStrategy) { + PersistenceCronStrategyDTO cronStrategyDTO = new PersistenceCronStrategyDTO(); + cronStrategyDTO.name = cronStrategy.getName(); + cronStrategyDTO.cronExpression = cronStrategy.getCronExpression(); + return cronStrategyDTO; + } + + private static PersistenceFilterDTO mapPersistenceThresholdFilter(PersistenceThresholdFilter thresholdFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = thresholdFilter.getName(); + filterDTO.value = thresholdFilter.getValue(); + filterDTO.unit = thresholdFilter.getUnit(); + return filterDTO; + } + + private static PersistenceFilterDTO mapPersistenceTimeFilter(PersistenceTimeFilter persistenceTimeFilter) { + PersistenceFilterDTO filterDTO = new PersistenceFilterDTO(); + filterDTO.name = persistenceTimeFilter.getName(); + filterDTO.value = new BigDecimal(persistenceTimeFilter.getValue()); + filterDTO.unit = persistenceTimeFilter.getUnit(); + return filterDTO; + } +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java new file mode 100644 index 00000000000..942097f0900 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationProvider.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2023 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.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Provider; + +/** + * The {@link PersistenceServiceConfigurationProvider} is an interface for persistence service configuration providers + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface PersistenceServiceConfigurationProvider extends Provider { +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java new file mode 100644 index 00000000000..5435f5b8559 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistry.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 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.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Registry; + +/** + * The {@link PersistenceServiceConfigurationRegistry} is the central place to store persistence service configurations. + * Configurations are registered through {@link PersistenceServiceConfigurationProvider}. + * Because the {@link org.openhab.core.persistence.internal.PersistenceManager} implementation needs to listen to + * different registries, the {@link PersistenceServiceConfigurationRegistryChangeListener} can be used to add listeners + * to this registry. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface PersistenceServiceConfigurationRegistry extends Registry { + void addRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener); + + void removeRegistryChangeListener(PersistenceServiceConfigurationRegistryChangeListener listener); +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java new file mode 100644 index 00000000000..4acf4d6adf1 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/registry/PersistenceServiceConfigurationRegistryChangeListener.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 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.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PersistenceServiceConfigurationRegistryChangeListener} is an interface that can be implemented by services + * that need to listen to the {@link PersistenceServiceConfigurationRegistry} when more than one registry with different + * types is used. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface PersistenceServiceConfigurationRegistryChangeListener { + /** + * Notifies the listener that a single element has been added. + * + * @param element the element that has been added + */ + void added(PersistenceServiceConfiguration element); + + /** + * Notifies the listener that a single element has been removed. + * + * @param element the element that has been removed + */ + void removed(PersistenceServiceConfiguration element); + + /** + * Notifies the listener that a single element has been updated. + * + * @param element the new element + * @param oldElement the element that has been updated + */ + void updated(PersistenceServiceConfiguration oldElement, PersistenceServiceConfiguration element); +} diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java index 375f43e4024..4cbf6ca659a 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/strategy/PersistenceStrategy.java @@ -12,6 +12,7 @@ */ package org.openhab.core.persistence.strategy; +import java.util.Map; import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -28,6 +29,9 @@ public static class Globals { public static final PersistenceStrategy UPDATE = new PersistenceStrategy("everyUpdate"); public static final PersistenceStrategy CHANGE = new PersistenceStrategy("everyChange"); public static final PersistenceStrategy RESTORE = new PersistenceStrategy("restoreOnStartup"); + + public static final Map STRATEGIES = Map.of(UPDATE.name, UPDATE, CHANGE.name, + CHANGE, RESTORE.name, RESTORE); } private final String name; @@ -44,7 +48,7 @@ public String getName() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + name.hashCode(); return result; } @@ -56,14 +60,10 @@ public boolean equals(final @Nullable Object obj) { if (obj == null) { return false; } - if (!(obj instanceof PersistenceStrategy)) { - return false; - } - final PersistenceStrategy other = (PersistenceStrategy) obj; - if (!Objects.equals(name, other.name)) { + if (!(obj instanceof final PersistenceStrategy other)) { return false; } - return true; + return Objects.equals(name, other.name); } @Override diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java index 419e1aec34a..f497a542559 100644 --- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import javax.measure.Unit; @@ -97,7 +98,7 @@ public State getState() { @Override public String getName() { - return filter.getItemName(); + return Objects.requireNonNull(filter.getItemName()); } }); } @@ -132,14 +133,14 @@ public ZonedDateTime getTimestamp() { @Override public State getState() { - Item item = itemRegistry.get(filter.getItemName()); + Item item = itemRegistry.get(Objects.requireNonNull(filter.getItemName())); Unit unit = item instanceof NumberItem ni ? ni.getUnit() : null; return unit == null ? new DecimalType(year) : QuantityType.valueOf(year, unit); } @Override public String getName() { - return filter.getItemName(); + return Objects.requireNonNull(filter.getItemName()); } }); } diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java new file mode 100644 index 00000000000..bcefefb6cfd --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceThresholdFilterTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2023 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.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.math.BigDecimal; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; +import org.openhab.core.types.util.UnitUtils; + +/** + * The {@link PersistenceThresholdFilterTest} contains tests for {@link PersistenceThresholdFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceThresholdFilterTest { + private static final String ITEM_NAME_1 = "itemName1"; + private static final String ITEM_NAME_2 = "itemName2"; + + @Test + public void differentItemSameValue() { + filterTest(ITEM_NAME_2, DecimalType.ZERO, DecimalType.ZERO, "", true); + } + + @ParameterizedTest + @MethodSource("argumentProvider") + public void filterTest(State state1, State state2, String unit, boolean expected) { + filterTest(ITEM_NAME_1, state1, state2, unit, expected); + } + + private static Stream argumentProvider() { + return Stream.of(// + // same item, same value -> false + Arguments.of(DecimalType.ZERO, DecimalType.ZERO, "", false), + // plain decimal, below threshold, absolute + Arguments.of(DecimalType.ZERO, DecimalType.valueOf("5"), "", false), + // plain decimal, above threshold, absolute + Arguments.of(DecimalType.ZERO, DecimalType.valueOf("15"), "", true), + // plain decimal, below threshold, relative + Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("9.5"), "%", false), + // plain decimal, above threshold, relative + Arguments.of(DecimalType.valueOf("10.0"), DecimalType.valueOf("11.5"), "%", true), + // quantity type, below threshold, relative + Arguments.of(new QuantityType<>("15 A"), new QuantityType<>("14000 mA"), "%", false), + // quantity type, above threshold, relative + Arguments.of(new QuantityType<>("2000 mbar"), new QuantityType<>("2.6 bar"), "%", true), + // quantity type, below threshold, absolute, no unit + Arguments.of(new QuantityType<>("100 K"), new QuantityType<>("105 K"), "", false), + // quantity type, above threshold, absolute, no unit + Arguments.of(new QuantityType<>("20 V"), new QuantityType<>("9000 mV"), "", true), + // quantity type, below threshold, absolute, with unit + Arguments.of(new QuantityType<>("10 m"), new QuantityType<>("10.002 m"), "mm", false), + // quantity type, above threshold, absolute, with unit + Arguments.of(new QuantityType<>("-10 °C"), new QuantityType<>("5 °C"), "K", true)); + } + + private void filterTest(String item2name, State state1, State state2, String unit, boolean expected) { + String itemType = "Number"; + if (state1 instanceof QuantityType q) { + itemType += ":" + UnitUtils.getDimensionName(q.getUnit()); + } + + NumberItem item1 = new NumberItem(itemType, PersistenceThresholdFilterTest.ITEM_NAME_1); + NumberItem item2 = new NumberItem(itemType, item2name); + + item1.setState(state1); + item2.setState(state2); + + PersistenceFilter filter = new PersistenceThresholdFilter("test", BigDecimal.TEN, unit); + + assertThat(filter.apply(item1), is(true)); + filter.persisted(item1); + assertThat(filter.apply(item2), is(expected)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java new file mode 100644 index 00000000000..38b2879e729 --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/filter/PersistenceTimeFilterTest.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 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.filter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.items.StringItem; + +/** + * The {@link PersistenceTimeFilterTest} contains tests for {@link PersistenceTimeFilter} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PersistenceTimeFilterTest { + + @Test + public void testTimeFilter() throws InterruptedException { + PersistenceFilter filter = new PersistenceTimeFilter("test", 1, "s"); + + StringItem item = new StringItem("testItem"); + assertThat(filter.apply(item), is(true)); + filter.persisted(item); + + // immediate store returns false + assertThat(filter.apply(item), is(false)); + + // after interval returns true + Thread.sleep(1500); + assertThat(filter.apply(item), is(true)); + } +} diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java new file mode 100644 index 00000000000..9172dd3e83e --- /dev/null +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/internal/PersistenceManagerTest.java @@ -0,0 +1,405 @@ +/** + * Copyright (c) 2010-2023 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.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.common.SafeCaller; +import org.openhab.core.common.SafeCallerBuilder; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistenceItemConfiguration; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.QueryablePersistenceService; +import org.openhab.core.persistence.config.PersistenceAllConfig; +import org.openhab.core.persistence.config.PersistenceConfig; +import org.openhab.core.persistence.config.PersistenceGroupConfig; +import org.openhab.core.persistence.config.PersistenceItemConfig; +import org.openhab.core.persistence.filter.PersistenceFilter; +import org.openhab.core.persistence.filter.PersistenceThresholdFilter; +import org.openhab.core.persistence.registry.PersistenceServiceConfiguration; +import org.openhab.core.persistence.registry.PersistenceServiceConfigurationRegistry; +import org.openhab.core.persistence.strategy.PersistenceCronStrategy; +import org.openhab.core.persistence.strategy.PersistenceStrategy; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.scheduler.SchedulerRunnable; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyService; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link PersistenceManagerTest} contains tests for the {@link PersistenceManager} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class PersistenceManagerTest { + private static final String TEST_ITEM_NAME = "testItem"; + private static final String TEST_ITEM2_NAME = "testItem2"; + private static final String TEST_ITEM3_NAME = "testItem3"; + private static final String TEST_GROUP_ITEM_NAME = "groupItem"; + + private static final StringItem TEST_ITEM = new StringItem(TEST_ITEM_NAME); + private static final StringItem TEST_ITEM2 = new StringItem(TEST_ITEM2_NAME); + private static final NumberItem TEST_ITEM3 = new NumberItem(TEST_ITEM3_NAME); + private static final GroupItem TEST_GROUP_ITEM = new GroupItem(TEST_GROUP_ITEM_NAME); + + private static final State TEST_STATE = new StringType("testState1"); + + private static final HistoricItem TEST_HISTORIC_ITEM = new HistoricItem() { + @Override + public ZonedDateTime getTimestamp() { + return ZonedDateTime.now().minusDays(1); + } + + @Override + public State getState() { + return TEST_STATE; + } + + @Override + public String getName() { + return TEST_ITEM_NAME; + } + }; + + private static final String TEST_PERSISTENCE_SERVICE_ID = "testPersistenceService"; + private static final String TEST_QUERYABLE_PERSISTENCE_SERVICE_ID = "testQueryablePersistenceService"; + + private @NonNullByDefault({}) @Mock CronScheduler cronSchedulerMock; + private @NonNullByDefault({}) @Mock ScheduledCompletableFuture scheduledFutureMock; + private @NonNullByDefault({}) @Mock ItemRegistry itemRegistryMock; + private @NonNullByDefault({}) @Mock SafeCaller safeCallerMock; + private @NonNullByDefault({}) @Mock SafeCallerBuilder safeCallerBuilderMock; + private @NonNullByDefault({}) @Mock ReadyService readyServiceMock; + private @NonNullByDefault({}) @Mock PersistenceServiceConfigurationRegistry persistenceServiceConfigurationRegistryMock; + + private @NonNullByDefault({}) @Mock PersistenceService persistenceServiceMock; + private @NonNullByDefault({}) @Mock QueryablePersistenceService queryablePersistenceServiceMock; + + private @NonNullByDefault({}) PersistenceManager manager; + + @BeforeEach + public void setUp() throws ItemNotFoundException { + TEST_GROUP_ITEM.addMember(TEST_ITEM); + + // set initial states + TEST_ITEM.setState(UnDefType.NULL); + TEST_ITEM2.setState(UnDefType.NULL); + TEST_ITEM3.setState(DecimalType.ZERO); + TEST_GROUP_ITEM.setState(UnDefType.NULL); + + when(itemRegistryMock.getItem(TEST_GROUP_ITEM_NAME)).thenReturn(TEST_GROUP_ITEM); + when(itemRegistryMock.getItem(TEST_ITEM_NAME)).thenReturn(TEST_ITEM); + when(itemRegistryMock.getItem(TEST_ITEM2_NAME)).thenReturn(TEST_ITEM2); + when(itemRegistryMock.getItem(TEST_ITEM3_NAME)).thenReturn(TEST_ITEM3); + when(itemRegistryMock.getItems()).thenReturn(List.of(TEST_ITEM, TEST_ITEM2, TEST_ITEM3, TEST_GROUP_ITEM)); + when(persistenceServiceMock.getId()).thenReturn(TEST_PERSISTENCE_SERVICE_ID); + when(queryablePersistenceServiceMock.getId()).thenReturn(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID); + when(queryablePersistenceServiceMock.query(any())).thenReturn(List.of(TEST_HISTORIC_ITEM)); + + manager = new PersistenceManager(cronSchedulerMock, itemRegistryMock, safeCallerMock, readyServiceMock, + persistenceServiceConfigurationRegistryMock); + manager.addPersistenceService(persistenceServiceMock); + manager.addPersistenceService(queryablePersistenceServiceMock); + + clearInvocations(persistenceServiceMock, queryablePersistenceServiceMock); + } + + @Test + public void appliesToItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void doesNotApplyToItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToGroupItemWithItemConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_GROUP_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToItemWithGroupConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceGroupConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void doesNotApplyToItemWithGroupConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceGroupConfig(TEST_GROUP_ITEM_NAME), + PersistenceStrategy.Globals.UPDATE, null); + + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void appliesToItemWithAllConfig() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + manager.stateUpdated(TEST_ITEM2, TEST_STATE); + manager.stateUpdated(TEST_GROUP_ITEM, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verify(persistenceServiceMock).store(TEST_ITEM2, null); + verify(persistenceServiceMock).store(TEST_GROUP_ITEM, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void updatedStatePersistsEveryUpdate() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + manager.stateUpdated(TEST_ITEM, TEST_STATE); + + verify(persistenceServiceMock, times(2)).store(TEST_ITEM, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void updatedStateDoesNotPersistWithChangeStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.CHANGE, + null); + + manager.stateUpdated(TEST_ITEM, TEST_STATE); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void changedStatePersistsWithChangeStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.CHANGE, + null); + + manager.stateChanged(TEST_ITEM, UnDefType.UNDEF, TEST_STATE); + + verify(persistenceServiceMock).store(TEST_ITEM, null); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void changedStateDoesNotPersistWithUpdateStrategy() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + null); + + manager.stateChanged(TEST_ITEM, UnDefType.UNDEF, TEST_STATE); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void restoreOnStartupWhenItemNull() { + setupPersistence(new PersistenceAllConfig()); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + verify(readyServiceMock, timeout(1000)).markReady(any()); + + assertThat(TEST_ITEM2.getState(), is(TEST_STATE)); + assertThat(TEST_ITEM.getState(), is(TEST_STATE)); + assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE)); + + verify(queryablePersistenceServiceMock, times(3)).query(any()); + + verifyNoMoreInteractions(queryablePersistenceServiceMock); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void noRestoreOnStartupWhenItemNotNull() { + setupPersistence(new PersistenceAllConfig()); + + // set TEST_ITEM state to a value + StringType initialValue = new StringType("value"); + TEST_ITEM.setState(initialValue); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + verify(readyServiceMock, timeout(1000)).markReady(any()); + + assertThat(TEST_ITEM.getState(), is(initialValue)); + assertThat(TEST_ITEM2.getState(), is(TEST_STATE)); + assertThat(TEST_GROUP_ITEM.getState(), is(TEST_STATE)); + + verify(queryablePersistenceServiceMock, times(2)).query(any()); + + verifyNoMoreInteractions(queryablePersistenceServiceMock); + verifyNoMoreInteractions(persistenceServiceMock); + } + + @Test + public void cronStrategyIsScheduledAndCancelledAndPersistsValue() throws Exception { + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(SchedulerRunnable.class); + when(cronSchedulerMock.schedule(runnableCaptor.capture(), any())).thenReturn(scheduledFutureMock); + + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM3_NAME), + new PersistenceCronStrategy("withoutFilter", "0 0 * * * ?"), null); + addConfiguration(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID, new PersistenceItemConfig(TEST_ITEM3_NAME), + new PersistenceCronStrategy("withFilter", "0 * * * * ?"), + new PersistenceThresholdFilter("test", BigDecimal.TEN, "")); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + + verify(readyServiceMock, timeout(1000)).markReady(any()); + List runnables = runnableCaptor.getAllValues(); + assertThat(runnables.size(), is(2)); + runnables.get(0).run(); + runnables.get(0).run(); + runnables.get(1).run(); + runnables.get(1).run(); + + manager.deactivate(); + + verify(cronSchedulerMock, times(2)).schedule(any(), any()); + verify(scheduledFutureMock, times(2)).cancel(true); + // no filter - persist everything + verify(persistenceServiceMock, times(2)).store(TEST_ITEM3, null); + // filter - persist filtered value + verify(queryablePersistenceServiceMock, times(1)).store(TEST_ITEM3, null); + } + + @Test + public void cronStrategyIsProperlyUpdated() { + when(cronSchedulerMock.schedule(any(), any())).thenReturn(scheduledFutureMock); + + PersistenceServiceConfiguration configuration = addConfiguration(TEST_PERSISTENCE_SERVICE_ID, + new PersistenceItemConfig(TEST_ITEM_NAME), new PersistenceCronStrategy("everyHour", "0 0 * * * ?"), + null); + + manager.onReadyMarkerAdded(new ReadyMarker("", "")); + + verify(readyServiceMock, timeout(1000)).markReady(any()); + + manager.updated(configuration, configuration); + manager.deactivate(); + + verify(cronSchedulerMock, times(2)).schedule(any(), any()); + verify(scheduledFutureMock, times(2)).cancel(true); + } + + @Test + public void filterAppliesOnStateUpdate() { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, new PersistenceAllConfig(), PersistenceStrategy.Globals.UPDATE, + new PersistenceThresholdFilter("test", BigDecimal.TEN, "")); + + manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO); + manager.stateUpdated(TEST_ITEM3, DecimalType.ZERO); + + verify(persistenceServiceMock, times(1)).store(TEST_ITEM3, null); + + verifyNoMoreInteractions(persistenceServiceMock); + } + + /** + * Add a configuration for restoring TEST_ITEM and mock the SafeCaller + */ + private void setupPersistence(PersistenceConfig itemConfig) { + addConfiguration(TEST_PERSISTENCE_SERVICE_ID, itemConfig, PersistenceStrategy.Globals.RESTORE, null); + addConfiguration(TEST_QUERYABLE_PERSISTENCE_SERVICE_ID, itemConfig, PersistenceStrategy.Globals.RESTORE, null); + + when(safeCallerMock.create(queryablePersistenceServiceMock, QueryablePersistenceService.class)) + .thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.onTimeout(any())).thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.onException(any())).thenReturn(safeCallerBuilderMock); + when(safeCallerBuilderMock.build()).thenReturn(queryablePersistenceServiceMock); + } + + /** + * Add a configuration to the manager + * + * @param serviceId the persistence service id + * @param itemConfig the item configuration + * @param strategy the strategy + * @param filter a persistence filter + * @return the added strategy + */ + private PersistenceServiceConfiguration addConfiguration(String serviceId, PersistenceConfig itemConfig, + PersistenceStrategy strategy, @Nullable PersistenceFilter filter) { + List filters = filter != null ? List.of(filter) : List.of(); + + PersistenceItemConfiguration itemConfiguration = new PersistenceItemConfiguration(List.of(itemConfig), null, + List.of(strategy), filters); + + List strategies = PersistenceStrategy.Globals.STRATEGIES.containsValue(strategy) + ? List.of() + : List.of(strategy); + + PersistenceServiceConfiguration serviceConfiguration = new PersistenceServiceConfiguration(serviceId, + List.of(itemConfiguration), List.of(), strategies, filters); + manager.added(serviceConfiguration); + + return serviceConfiguration; + } +}