diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java index 2ffb7adb75e..5d8dcebb401 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java @@ -74,7 +74,7 @@ import org.openhab.core.automation.rest.internal.dto.EnrichedRuleDTOMapper; import org.openhab.core.automation.util.ModuleBuilder; import org.openhab.core.automation.util.RuleBuilder; -import org.openhab.core.common.registry.RegistryChangeListener; +import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.config.core.ConfigUtil; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.Event; @@ -135,7 +135,8 @@ public class RuleResource implements RESTResource { private final RuleManager ruleManager; private final RuleRegistry ruleRegistry; private final ManagedRuleProvider managedRuleProvider; - private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener(); + private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( + () -> cacheableListLastModified = null); private @Context @NonNullByDefault({}) UriInfo uriInfo; private @Nullable Date cacheableListLastModified = null; @@ -608,26 +609,4 @@ public Response setModuleConfigParam(@PathParam("ruleUID") @Parameter(descriptio return null; } } - - private void resetStaticListLastModified() { - cacheableListLastModified = null; - } - - private class ResetLastModifiedChangeListener implements RegistryChangeListener { - - @Override - public void added(Rule element) { - resetStaticListLastModified(); - } - - @Override - public void removed(Rule element) { - resetStaticListLastModified(); - } - - @Override - public void updated(Rule oldElement, Rule element) { - resetStaticListLastModified(); - } - } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index c17f5bd6c73..570eb0ed7ba 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -57,7 +57,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; -import org.openhab.core.common.registry.RegistryChangeListener; +import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.events.EventPublisher; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; @@ -74,7 +74,6 @@ import org.openhab.core.items.ItemBuilderFactory; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; -import org.openhab.core.items.ItemRegistryChangeListener; import org.openhab.core.items.ManagedItemProvider; import org.openhab.core.items.Metadata; import org.openhab.core.items.MetadataKey; @@ -183,8 +182,15 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; private final SemanticTagRegistry semanticTagRegistry; - private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener(); - private final RegistryChangeListener resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener(); + + private void resetCacheableListsLastModified() { + this.cacheableListsLastModified.clear(); + } + + private final RegistryChangedRunnableListener resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>( + this::resetCacheableListsLastModified); + private final RegistryChangedRunnableListener resetLastModifiedMetadataChangeListener = new RegistryChangedRunnableListener<>( + this::resetCacheableListsLastModified); private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>(); @@ -990,48 +996,4 @@ private void addMetadata(EnrichedItemDTO dto, Set namespaces, @Nullable private boolean isEditable(String itemName) { return managedItemProvider.get(itemName) != null; } - - private void resetCacheableListsLastModified() { - this.cacheableListsLastModified.clear(); - } - - private class ResetLastModifiedItemChangeListener implements ItemRegistryChangeListener { - @Override - public void added(Item element) { - resetCacheableListsLastModified(); - } - - @Override - public void allItemsChanged(Collection oldItemNames) { - resetCacheableListsLastModified(); - } - - @Override - public void removed(Item element) { - resetCacheableListsLastModified(); - } - - @Override - public void updated(Item oldElement, Item element) { - resetCacheableListsLastModified(); - } - } - - private class ResetLastModifiedMetadataChangeListener implements RegistryChangeListener { - - @Override - public void added(Metadata element) { - resetCacheableListsLastModified(); - } - - @Override - public void removed(Metadata element) { - resetCacheableListsLastModified(); - } - - @Override - public void updated(Metadata oldElement, Metadata element) { - resetCacheableListsLastModified(); - } - } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java index b3d954f3d3c..cc4d558b47e 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java @@ -12,9 +12,12 @@ */ package org.openhab.core.io.rest.core.internal.tag; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Comparator; -import java.util.List; +import java.util.Date; import java.util.Locale; +import java.util.stream.Stream; import javax.annotation.security.RolesAllowed; import javax.ws.rs.Consumes; @@ -26,9 +29,11 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; @@ -36,16 +41,19 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; +import org.openhab.core.io.rest.Stream2JSONInputStream; import org.openhab.core.semantics.ManagedSemanticTagProvider; import org.openhab.core.semantics.SemanticTag; import org.openhab.core.semantics.SemanticTagImpl; import org.openhab.core.semantics.SemanticTagRegistry; 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.jaxrs.whiteboard.JaxrsWhiteboardConstants; import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; @@ -83,6 +91,10 @@ public class TagResource implements RESTResource { private final LocaleService localeService; private final SemanticTagRegistry semanticTagRegistry; private final ManagedSemanticTagProvider managedSemanticTagProvider; + private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( + () -> lastModified = null); + + private @Nullable Date lastModified = null; // TODO pattern in @Path @@ -93,6 +105,13 @@ public TagResource(final @Reference LocaleService localeService, this.localeService = localeService; this.semanticTagRegistry = semanticTagRegistry; this.managedSemanticTagProvider = managedSemanticTagProvider; + + this.semanticTagRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); + } + + @Deactivate + void deactivate() { + this.semanticTagRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener); } @GET @@ -100,14 +119,29 @@ public TagResource(final @Reference LocaleService localeService, @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSemanticTags", summary = "Get all available semantic tags.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))) }) - public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, + public Response getTags(final @Context Request request, final @Context UriInfo uriInfo, + final @Context HttpHeaders httpHeaders, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) { + if (lastModified != null) { + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + final Locale locale = localeService.getLocale(language); - List tagsDTO = semanticTagRegistry.getAll().stream() + Stream tagsStream = semanticTagRegistry.getAll().stream() .sorted(Comparator.comparing(SemanticTag::getUID)) - .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))).toList(); - return JSONResponse.createResponse(Status.OK, tagsDTO, null); + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))); + return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified).cacheControl(cc).build(); } @GET @@ -117,19 +151,33 @@ public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeade @Operation(operationId = "getSemanticTagAndSubTags", summary = "Gets a semantic tag and its sub tags.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))), @ApiResponse(responseCode = "404", description = "Semantic tag not found.") }) - public Response getTagAndSubTags( + public Response getTagAndSubTags(final @Context Request request, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @PathParam("tagId") @Parameter(description = "tag id") String tagId) { + if (lastModified != null) { + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + final Locale locale = localeService.getLocale(language); String uid = tagId.trim(); SemanticTag tag = semanticTagRegistry.get(uid); if (tag != null) { - List tagsDTO = semanticTagRegistry.getSubTree(tag).stream() + Stream tagsStream = semanticTagRegistry.getSubTree(tag).stream() .sorted(Comparator.comparing(SemanticTag::getUID)) - .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))) - .toList(); - return JSONResponse.createResponse(Status.OK, tagsDTO, null); + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))); + return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified).cacheControl(cc) + .build(); } else { return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!"); } @@ -187,8 +235,6 @@ public Response create( public Response remove( @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @PathParam("tagId") @Parameter(description = "tag id") String tagId) { - final Locale locale = localeService.getLocale(language); - String uid = tagId.trim(); // check whether tag exists and throw 404 if not diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java index 84816fa841a..cb517fb9353 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java @@ -54,7 +54,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; -import org.openhab.core.common.registry.RegistryChangeListener; +import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.config.core.ConfigUtil; @@ -171,7 +171,8 @@ public class ThingResource implements RESTResource { private final ThingRegistry thingRegistry; private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService; private final ThingTypeRegistry thingTypeRegistry; - private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener(); + private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( + () -> cacheableListLastModified = null); private @Context @NonNullByDefault({}) UriInfo uriInfo; private @Nullable Date cacheableListLastModified = null; @@ -890,26 +891,4 @@ private URI getConfigDescriptionURI(ChannelUID channelUID) { throw new BadRequestException("Invalid URI syntax: " + uriString); } } - - private void resetCacheableListLastModified() { - cacheableListLastModified = null; - } - - private class ResetLastModifiedChangeListener implements RegistryChangeListener { - - @Override - public void added(Thing element) { - resetCacheableListLastModified(); - } - - @Override - public void removed(Thing element) { - resetCacheableListLastModified(); - } - - @Override - public void updated(Thing oldElement, Thing element) { - resetCacheableListLastModified(); - } - } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/RegistryChangedRunnableListener.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/RegistryChangedRunnableListener.java new file mode 100644 index 00000000000..389c89e5812 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/registry/RegistryChangedRunnableListener.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.common.registry; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link RegistryChangedRunnableListener} can be added to {@link Registry} services, to execute a given + * {@link Runnable} on all types of changes. + * + * @author Florian Hotze - Initial contribution + * + * @param type of the element in the registry + */ +@NonNullByDefault +public class RegistryChangedRunnableListener implements RegistryChangeListener { + final Runnable runnable; + + public RegistryChangedRunnableListener(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void added(E element) { + runnable.run(); + } + + @Override + public void removed(E element) { + runnable.run(); + } + + @Override + public void updated(E oldElement, E newElement) { + runnable.run(); + } +}