mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-25 11:45:49 +01:00
"Cacheability" option for critical REST resources (#3335)
* Closes #3329. This implements a new optional `cacheable` parameter for these REST endpoints: - `/rest/items` - `/rest/things` - `/rest/rules` When this parameter is set, a flat list of all elements excluding non-cacheable fields (e.g. "state", "transformedState", "stateDescription", "commandDescription" for items, "statusInfo", "firmwareStatus", "properties" for things, "status" for rules) will be retrieved along with a `Last-Modified` HTTP response header. When unknown, the Last-Modified header will be set to the date of the request. Also only when this parameter is set, and a `If-Modified-Since` header is found in the request, that header will be compared to the last known modified date for the corresponding cacheable list. The last modified date will be reset when any change is made on the elements of the underlying registry. If the `If-Modified-Since` date is equal or more recent than the last modified date, then a 304 Not Modified response with no content will be served instead of the usual 200 OK, informing the client that its cache is still valid at the provided date. All other request parameters will be ignored except for "metadata" in the `/rest/items` endpoint. When a metadata selector is set, the resulting item list will be considered like a completely different resource, i.e. it will have its own last modified date. Regarding metadata, the approach to invalidating last modified dates is very conservative: when any metadata is changed, all cacheable lists of items will have their last modified date reset even if the change was in a metadata namespace that wasn't requested. This also implements the abovedescribed behavior for the `/rest/ui/components/{namespace}` endpoint, but no `cacheable` parameter is necessary. The last modified date is tracked by namespace. Signed-off-by: Yannick Schaus <github@schaus.net>
This commit is contained in:
parent
885a8548c8
commit
6e83d3f8de
@ -17,9 +17,11 @@ import static org.openhab.core.automation.RulePredicates.*;
|
||||
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 java.util.stream.Stream;
|
||||
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.Path;
|
||||
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.EnrichedRuleDTO;
|
||||
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.io.rest.Stream2JSONInputStream;
|
||||
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 class RuleResource implements RESTResource {
|
||||
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 class RuleResource implements RESTResource {
|
||||
@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<String> 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<String> 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<EnrichedRuleDTO> 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<Rule> p = r -> true;
|
||||
|
||||
@ -567,4 +608,26 @@ public class RuleResource implements RESTResource {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetStaticListLastModified() {
|
||||
cacheableListLastModified = null;
|
||||
}
|
||||
|
||||
private class ResetLastModifiedChangeListener implements RegistryChangeListener<Rule> {
|
||||
|
||||
@Override
|
||||
public void added(Rule element) {
|
||||
resetStaticListLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Rule element) {
|
||||
resetStaticListLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Rule oldElement, Rule element) {
|
||||
resetStaticListLastModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.PathParam;
|
||||
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 javax.ws.rs.core.UriInfo;
|
||||
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.Item;
|
||||
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.State;
|
||||
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 @@ public class ItemResource implements RESTResource {
|
||||
private final ManagedItemProvider managedItemProvider;
|
||||
private final MetadataRegistry metadataRegistry;
|
||||
private final MetadataSelectorMatcher metadataSelectorMatcher;
|
||||
private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener();
|
||||
private final RegistryChangeListener<Metadata> resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener();
|
||||
|
||||
private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>();
|
||||
|
||||
@Activate
|
||||
public ItemResource(//
|
||||
@ -193,6 +205,15 @@ public class ItemResource implements RESTResource {
|
||||
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 @@ public class ItemResource implements RESTResource {
|
||||
@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<String> 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<EnrichedItemDTO> 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<EnrichedItemDTO> itemStream = getItems(type, tags).stream() //
|
||||
.map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) //
|
||||
.peek(dto -> addMetadata(dto, namespaces, null)) //
|
||||
@ -935,4 +986,48 @@ public class ItemResource implements RESTResource {
|
||||
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<String> oldItemNames) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Item element) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Item oldElement, Item element) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
}
|
||||
|
||||
private class ResetLastModifiedMetadataChangeListener implements RegistryChangeListener<Metadata> {
|
||||
|
||||
@Override
|
||||
public void added(Metadata element) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Metadata element) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Metadata oldElement, Metadata element) {
|
||||
resetCacheableListsLastModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,12 @@ package org.openhab.core.io.rest.core.internal.thing;
|
||||
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.Path;
|
||||
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 javax.ws.rs.core.UriInfo;
|
||||
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.type.ThingTypeRegistry;
|
||||
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 class ThingResource implements RESTResource {
|
||||
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 class ThingResource implements RESTResource {
|
||||
@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<EnrichedThingDTO> 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 @@ public class ThingResource implements RESTResource {
|
||||
throw new BadRequestException("Invalid URI syntax: " + uriString);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetCacheableListLastModified() {
|
||||
cacheableListLastModified = null;
|
||||
}
|
||||
|
||||
private class ResetLastModifiedChangeListener implements RegistryChangeListener<Thing> {
|
||||
|
||||
@Override
|
||||
public void added(Thing element) {
|
||||
resetCacheableListLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Thing element) {
|
||||
resetCacheableListLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Thing oldElement, Thing element) {
|
||||
resetCacheableListLastModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.Path;
|
||||
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.Tile;
|
||||
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<String, Date> lastModifiedDates = new HashMap<>();
|
||||
private Map<String, RegistryChangeListener<RootUIComponent>> registryChangeListeners = new HashMap<>();
|
||||
|
||||
@Activate
|
||||
public UIResource( //
|
||||
final @Reference UIComponentRegistryFactory componentRegistryFactory,
|
||||
@ -92,6 +104,14 @@ public class UIResource implements RESTResource {
|
||||
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 class UIResource implements RESTResource {
|
||||
@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<RootUIComponent> components = registry.getAll().stream();
|
||||
@ -126,8 +146,33 @@ public class UIResource implements RESTResource {
|
||||
}
|
||||
return component;
|
||||
});
|
||||
return Response.ok(new Stream2JSONInputStream(components)).build();
|
||||
} else {
|
||||
if (!registryChangeListeners.containsKey(namespace)) {
|
||||
RegistryChangeListener<RootUIComponent> 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 class UIResource implements RESTResource {
|
||||
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<RootUIComponent> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import java.util.Set;
|
||||
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 class ItemResourceOSGiTest extends JavaOSGiTest {
|
||||
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 class ItemResourceOSGiTest extends JavaOSGiTest {
|
||||
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 class ItemResourceOSGiTest extends JavaOSGiTest {
|
||||
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 class ItemResourceOSGiTest extends JavaOSGiTest {
|
||||
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));
|
||||
|
Loading…
Reference in New Issue
Block a user