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 ac226f289e9..2ffb7adb75e 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 @@ -17,9 +17,11 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -29,6 +31,7 @@ import javax.annotation.security.RolesAllowed; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -36,8 +39,10 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; 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.SecurityContext; @@ -69,6 +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.config.core.ConfigUtil; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.Event; @@ -80,6 +86,7 @@ import org.openhab.core.library.types.DateTimeType; 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; @@ -128,8 +135,10 @@ public class RuleResource implements RESTResource { private final RuleManager ruleManager; private final RuleRegistry ruleRegistry; private final ManagedRuleProvider managedRuleProvider; + private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener(); private @Context @NonNullByDefault({}) UriInfo uriInfo; + private @Nullable Date cacheableListLastModified = null; @Activate public RuleResource( // @@ -141,6 +150,13 @@ public RuleResource( // this.ruleManager = ruleManager; this.ruleRegistry = ruleRegistry; this.managedRuleProvider = managedRuleProvider; + + this.ruleRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); + } + + @Deactivate + void deactivate() { + this.ruleRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener); } @GET @@ -148,13 +164,38 @@ public RuleResource( // @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getRules", summary = "Get available rules, optionally filtered by tags and/or prefix.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedRuleDTO.class)))) }) - public Response get(@Context SecurityContext securityContext, @QueryParam("prefix") final @Nullable String prefix, - @QueryParam("tags") final @Nullable List tags, - @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) { + public Response get(@Context SecurityContext securityContext, @Context Request request, + @QueryParam("prefix") final @Nullable String prefix, @QueryParam("tags") final @Nullable List tags, + @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary, + @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and honors the If-Modified-Since header, all other parameters are ignored") boolean staticDataOnly) { + if ((summary == null || !summary) && !securityContext.isUserInRole(Role.ADMIN)) { // users may only access the summary return JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Authentication required"); } + + if (staticDataOnly) { + if (cacheableListLastModified != null) { + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(cacheableListLastModified); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + cacheableListLastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + } + + Stream rules = ruleRegistry.stream() + .map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider)); + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + rules = dtoMapper.limitToFields(rules, "uid,templateUID,name,visibility,description,tags,editable"); + return Response.ok(new Stream2JSONInputStream(rules)).lastModified(cacheableListLastModified) + .cacheControl(cc).build(); + } + // match all Predicate p = r -> true; @@ -567,4 +608,26 @@ 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 46d734be7a2..28e1b6e5ef0 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 @@ -12,9 +12,12 @@ */ package org.openhab.core.io.rest.core.internal.item; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,9 +43,11 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; +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.ResponseBuilder; import javax.ws.rs.core.Response.Status; @@ -52,6 +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.events.EventPublisher; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; @@ -68,6 +74,7 @@ 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; @@ -88,6 +95,7 @@ import org.openhab.core.types.TypeParser; 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; @@ -174,6 +182,10 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final ManagedItemProvider managedItemProvider; private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; + private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener(); + private final RegistryChangeListener resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener(); + + private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>(); @Activate public ItemResource(// @@ -193,6 +205,15 @@ public ItemResource(// this.managedItemProvider = managedItemProvider; this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; + + this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); + this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener); + } + + @Deactivate + void deactivate() { + this.itemRegistry.removeRegistryChangeListener(resetLastModifiedItemChangeListener); + this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener); } private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) { @@ -207,17 +228,47 @@ private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeade @Operation(operationId = "getItems", summary = "Get all available items.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedItemDTO.class)))) }) public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, + @Context Request request, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @QueryParam("type") @Parameter(description = "item type filter") @Nullable String type, @QueryParam("tags") @Parameter(description = "item tag filter") @Nullable String tags, @DefaultValue(".*") @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (returns all if no value given)") @Nullable String namespaceSelector, @DefaultValue("false") @QueryParam("recursive") @Parameter(description = "get member items recursively") boolean recursive, - @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields) { + @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields, + @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and checks the If-Modified-Since header, all other parameters are ignored except \"metadata\"") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders); + if (staticDataOnly) { + Date lastModifiedDate = Date.from(Instant.now()); + if (cacheableListsLastModified.containsKey(namespaceSelector)) { + lastModifiedDate = cacheableListsLastModified.get(namespaceSelector); + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModifiedDate); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModifiedDate = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + cacheableListsLastModified.put(namespaceSelector, lastModifiedDate); + } + + Stream itemStream = getItems(null, null).stream() // + .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale)) // + .peek(dto -> addMetadata(dto, namespaces, null)) // + .peek(dto -> dto.editable = isEditable(dto.name)); + itemStream = dtoMapper.limitToFields(itemStream, + "name,label,type,groupType,function,category,editable,groupNames,link,tags,metadata"); + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + return Response.ok(new Stream2JSONInputStream(itemStream)).lastModified(lastModifiedDate).cacheControl(cc) + .build(); + } + Stream itemStream = getItems(type, tags).stream() // .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) // .peek(dto -> addMetadata(dto, namespaces, null)) // @@ -935,4 +986,48 @@ 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/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 324482635ff..84816fa841a 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 @@ -15,9 +15,12 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -39,9 +42,11 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +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; @@ -49,6 +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.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.config.core.ConfigUtil; @@ -100,6 +106,7 @@ import org.openhab.core.thing.util.ThingHelper; 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; @@ -164,8 +171,10 @@ public class ThingResource implements RESTResource { private final ThingRegistry thingRegistry; private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService; private final ThingTypeRegistry thingTypeRegistry; + private final ResetLastModifiedChangeListener resetLastModifiedChangeListener = new ResetLastModifiedChangeListener(); private @Context @NonNullByDefault({}) UriInfo uriInfo; + private @Nullable Date cacheableListLastModified = null; @Activate public ThingResource( // @@ -198,6 +207,13 @@ public ThingResource( // this.thingRegistry = thingRegistry; this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService; this.thingTypeRegistry = thingTypeRegistry; + + this.thingRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); + } + + @Deactivate + void deactivate() { + this.thingRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener); } /** @@ -291,13 +307,34 @@ public Response create( @Operation(operationId = "getThings", summary = "Get all available things.", security = { @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedThingDTO.class), uniqueItems = true))) }) - public Response getAll( + public Response getAll(@Context Request request, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, - @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) { + @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary, + @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and checks the If-Modified-Since header") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); Stream thingStream = thingRegistry.stream().map(t -> convertToEnrichedThingDTO(t, locale)) .distinct(); + + if (staticDataOnly) { + if (cacheableListLastModified != null) { + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(cacheableListLastModified); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + cacheableListLastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + thingStream = dtoMapper.limitToFields(thingStream, "UID,label,bridgeUID,thingTypeUID,location,editable"); + return Response.ok(new Stream2JSONInputStream(thingStream)).lastModified(cacheableListLastModified) + .cacheControl(cc).build(); + } + if (summary != null && summary) { thingStream = dtoMapper.limitToFields(thingStream, "UID,label,bridgeUID,thingTypeUID,statusInfo,firmwareStatus,location,editable"); @@ -853,4 +890,26 @@ 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.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java b/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java index 166a3863dd5..eb34f73ce11 100644 --- a/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java +++ b/bundles/org.openhab.core.io.rest.ui/src/main/java/org/openhab/core/io/rest/ui/internal/UIResource.java @@ -13,7 +13,11 @@ package org.openhab.core.io.rest.ui.internal; import java.security.InvalidParameterException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -27,13 +31,17 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; 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 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.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; import org.openhab.core.io.rest.Stream2JSONInputStream; @@ -45,6 +53,7 @@ import org.openhab.core.ui.tiles.TileProvider; 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; @@ -84,6 +93,9 @@ public class UIResource implements RESTResource { private final UIComponentRegistryFactory componentRegistryFactory; private final TileProvider tileProvider; + private Map lastModifiedDates = new HashMap<>(); + private Map> registryChangeListeners = new HashMap<>(); + @Activate public UIResource( // final @Reference UIComponentRegistryFactory componentRegistryFactory, @@ -92,6 +104,14 @@ public UIResource( // this.tileProvider = tileProvider; } + @Deactivate + public void deactivate() { + registryChangeListeners.forEach((n, l) -> { + UIComponentRegistry registry = componentRegistryFactory.getRegistry(n); + registry.removeRegistryChangeListener(l); + }); + } + @GET @Path("/tiles") @Produces({ MediaType.APPLICATION_JSON }) @@ -107,7 +127,7 @@ public Response getAll() { @Produces({ MediaType.APPLICATION_JSON }) @Operation(operationId = "getRegisteredUIComponentsInNamespace", summary = "Get all registered UI components in the specified namespace.", responses = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RootUIComponent.class)))) }) - public Response getAllComponents(@PathParam("namespace") String namespace, + public Response getAllComponents(@Context Request request, @PathParam("namespace") String namespace, @QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) { UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace); Stream components = registry.getAll().stream(); @@ -126,8 +146,33 @@ public Response getAllComponents(@PathParam("namespace") String namespace, } return component; }); + return Response.ok(new Stream2JSONInputStream(components)).build(); + } else { + if (!registryChangeListeners.containsKey(namespace)) { + RegistryChangeListener changeListener = new ResetLastModifiedChangeListener(namespace); + registryChangeListeners.put(namespace, changeListener); + registry.addRegistryChangeListener(changeListener); + } + + Date lastModifiedDate = Date.from(Instant.now()); + if (lastModifiedDates.containsKey(namespace)) { + lastModifiedDate = lastModifiedDates.get(namespace); + Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModifiedDate); + if (responseBuilder != null) { + // send 304 Not Modified + return responseBuilder.build(); + } + } else { + lastModifiedDate = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + lastModifiedDates.put(namespace, lastModifiedDate); + } + + CacheControl cc = new CacheControl(); + cc.setMustRevalidate(true); + cc.setPrivate(true); + return Response.ok(new Stream2JSONInputStream(components)).lastModified(lastModifiedDate).cacheControl(cc) + .build(); } - return Response.ok(new Stream2JSONInputStream(components)).build(); } @GET @@ -208,4 +253,32 @@ public Response deleteComponent(@PathParam("namespace") String namespace, private TileDTO toTileDTO(Tile tile) { return new TileDTO(tile.getName(), tile.getUrl(), tile.getOverlay(), tile.getImageUrl()); } + + private void resetLastModifiedDate(String namespace) { + lastModifiedDates.remove(namespace); + } + + private class ResetLastModifiedChangeListener implements RegistryChangeListener { + + private String namespace; + + ResetLastModifiedChangeListener(String namespace) { + this.namespace = namespace; + } + + @Override + public void added(RootUIComponent element) { + resetLastModifiedDate(namespace); + } + + @Override + public void removed(RootUIComponent element) { + resetLastModifiedDate(namespace); + } + + @Override + public void updated(RootUIComponent oldElement, RootUIComponent element) { + resetLastModifiedDate(namespace); + } + } } diff --git a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java index 5e684f4ed62..b39d4a01192 100644 --- a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java +++ b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResourceOSGiTest.java @@ -30,6 +30,7 @@ import java.util.stream.Stream; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; @@ -94,6 +95,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest { private @Mock @NonNullByDefault({}) ItemProvider itemProviderMock; private @Mock @NonNullByDefault({}) UriBuilder uriBuilderMock; private @Mock @NonNullByDefault({}) UriInfo uriInfoMock; + private @Mock @NonNullByDefault({}) Request request; @BeforeEach public void beforeEach() { @@ -127,7 +129,8 @@ public void beforeEach() { public void shouldReturnUnicodeItems() throws IOException, TransformationException { item4.setLabel(ITEM_LABEL4); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, null, null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, null, null, false, + null, false); assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4)); } @@ -147,28 +150,31 @@ public void shouldFilterItemsByTag() throws Exception { item3.addTag("Tag2"); item4.addTag("Tag4"); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "Tag1", null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag1", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "Tag2", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag2", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME2, ITEM_NAME3)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "NotExistingTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "NotExistingTag", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @Test public void shouldFilterItemsByType() throws Exception { - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.SWITCH, null, - null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.SWITCH, + null, null, false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.DIMMER, null, null, false, - null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.DIMMER, null, + null, false, null, false); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME3)); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, CoreItemFactory.COLOR, null, null, false, - null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.COLOR, null, null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -176,15 +182,18 @@ public void shouldFilterItemsByType() throws Exception { public void shouldAddAndRemoveTags() throws Exception { managedItemProvider.add(new SwitchItem("Switch")); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, + false, null, false); assertThat(readItemNamesFromResponse(response), hasSize(0)); itemResource.addTag("Switch", "MyTag"); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasSize(1)); itemResource.removeTag("Switch", "MyTag"); - response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, null); + response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null, + false); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -192,8 +201,8 @@ public void shouldAddAndRemoveTags() throws Exception { public void shouldIncludeRequestedFieldsOnly() throws Exception { managedItemProvider.add(new SwitchItem("Switch")); itemResource.addTag("Switch", "MyTag"); - Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, null, null, "MyTag", null, false, - "type,name"); + Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, + false, "type,name", false); JsonElement result = JsonParser .parseString(new String(((InputStream) response.getEntity()).readAllBytes(), StandardCharsets.UTF_8));