[rest] Add caching for add-on resource (#4107)

* [rest] Introduce a CACHE_CONTROL constant
* [rest] Add caching for add-ons endpoint

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
Florian Hotze 2024-03-27 19:37:36 +01:00 committed by GitHub
parent bf8b131701
commit 7f47d825a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 95 additions and 80 deletions

View File

@ -38,7 +38,6 @@ 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;
@ -135,10 +134,10 @@ public class RuleResource implements RESTResource {
private final RuleRegistry ruleRegistry;
private final ManagedRuleProvider managedRuleProvider;
private final RegistryChangedRunnableListener<Rule> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> cacheableListLastModified = null);
() -> lastModified = null);
private @Context @NonNullByDefault({}) UriInfo uriInfo;
private @Nullable Date cacheableListLastModified = null;
private @Nullable Date lastModified = null;
@Activate
public RuleResource( //
@ -174,26 +173,22 @@ public class RuleResource implements RESTResource {
}
if (staticDataOnly) {
if (cacheableListLastModified != null) {
Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(cacheableListLastModified);
if (lastModified != null) {
Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified);
if (responseBuilder != null) {
// send 304 Not Modified
return responseBuilder.build();
}
} else {
cacheableListLastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
Stream<EnrichedRuleDTO> rules = ruleRegistry.stream()
.map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider));
CacheControl cc = new CacheControl();
cc.setNoCache(true);
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();
return Response.ok(new Stream2JSONInputStream(rules)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
// match all

View File

@ -16,7 +16,10 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.Collator;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -37,6 +40,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
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.UriInfo;
@ -45,6 +49,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.core.addon.Addon;
import org.openhab.core.addon.AddonEvent;
import org.openhab.core.addon.AddonEventFactory;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonInfoRegistry;
@ -59,6 +64,7 @@ import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.addon.AddonSuggestionService;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
@ -106,13 +112,14 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@SecurityRequirement(name = "oauth2", scopes = { "admin" })
@Tag(name = AddonResource.PATH_ADDONS)
@NonNullByDefault
public class AddonResource implements RESTResource {
public class AddonResource implements RESTResource, EventSubscriber {
private static final String THREAD_POOL_NAME = "addonService";
public static final String PATH_ADDONS = "addons";
public static final String DEFAULT_ADDON_SERVICE = "karaf";
private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(AddonEvent.TYPE);
private final Logger logger = LoggerFactory.getLogger(AddonResource.class);
private final Set<AddonService> addonServices = new CopyOnWriteArraySet<>();
@ -123,6 +130,8 @@ public class AddonResource implements RESTResource {
private final ConfigDescriptionRegistry configDescriptionRegistry;
private final AddonSuggestionService addonSuggestionService;
private @Nullable Date lastModified = null;
private @Context @NonNullByDefault({}) UriInfo uriInfo;
@Activate
@ -142,30 +151,59 @@ public class AddonResource implements RESTResource {
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addAddonService(AddonService featureService) {
this.addonServices.add(featureService);
lastModified = null;
}
protected void removeAddonService(AddonService featureService) {
this.addonServices.remove(featureService);
}
@Override
public Set<String> getSubscribedEventTypes() {
return SUBSCRIBED_EVENT_TYPES;
}
@Override
public void receive(Event event) {
lastModified = null;
}
private boolean lastModifiedIsValid() {
if (lastModified == null)
return false;
return (new Date().getTime() - lastModified.getTime()) <= 450 * 1000;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getAddons", summary = "Get all add-ons.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Addon.class)))),
@ApiResponse(responseCode = "404", description = "Service not found") })
public Response getAddon(
public Response getAddon(final @Context Request request,
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language,
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
Locale locale = localeService.getLocale(language);
if (lastModifiedIsValid()) {
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));
}
final Locale locale = localeService.getLocale(language);
if ("all".equals(serviceId)) {
return Response.ok(new Stream2JSONInputStream(getAllAddons(locale))).build();
return Response.ok(new Stream2JSONInputStream(getAllAddons(locale))).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
} else {
AddonService addonService = (serviceId != null) ? getServiceById(serviceId) : getDefaultService();
if (addonService == null) {
return Response.status(HttpStatus.NOT_FOUND_404).build();
}
return Response.ok(new Stream2JSONInputStream(addonService.getAddons(locale).stream())).build();
return Response.ok(new Stream2JSONInputStream(addonService.getAddons(locale).stream()))
.lastModified(lastModified).cacheControl(RESTConstants.CACHE_CONTROL).build();
}
}
@ -174,12 +212,23 @@ public class AddonResource implements RESTResource {
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getAddonTypes", summary = "Get all add-on types.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AddonType.class)))) })
public Response getServices(
public Response getServices(final @Context Request request,
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language) {
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
Locale locale = localeService.getLocale(language);
if (lastModifiedIsValid()) {
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));
}
final Locale locale = localeService.getLocale(language);
Stream<AddonServiceDTO> addonTypeStream = addonServices.stream().map(s -> convertToAddonServiceDTO(s, locale));
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
@GET

View File

@ -42,7 +42,6 @@ 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;
@ -182,16 +181,12 @@ public class ItemResource implements RESTResource {
private final MetadataSelectorMatcher metadataSelectorMatcher;
private final SemanticTagRegistry semanticTagRegistry;
private void resetCacheableListsLastModified() {
this.cacheableListsLastModified.clear();
}
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
this::resetCacheableListsLastModified);
() -> lastModified = null);
private final RegistryChangedRunnableListener<Metadata> resetLastModifiedMetadataChangeListener = new RegistryChangedRunnableListener<>(
this::resetCacheableListsLastModified);
() -> lastModified = null);
private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>();
private @Nullable Date lastModified = null;
@Activate
public ItemResource(//
@ -250,17 +245,14 @@ public class ItemResource implements RESTResource {
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 (lastModified != null) {
Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified);
if (responseBuilder != null) {
// send 304 Not Modified
return responseBuilder.build();
}
} else {
lastModifiedDate = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
cacheableListsLastModified.put(namespaceSelector, lastModifiedDate);
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
Stream<EnrichedItemDTO> itemStream = getItems(type, tags).stream() //
@ -270,12 +262,8 @@ public class ItemResource implements RESTResource {
itemStream = dtoMapper.limitToFields(itemStream,
"name,label,type,groupType,function,category,editable,groupNames,link,tags,metadata,commandDescription,stateDescription");
CacheControl cc = new CacheControl();
cc.setNoCache(true);
cc.setMustRevalidate(true);
cc.setPrivate(true);
return Response.ok(new Stream2JSONInputStream(itemStream)).lastModified(lastModifiedDate).cacheControl(cc)
.build();
return Response.ok(new Stream2JSONInputStream(itemStream)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
Stream<EnrichedItemDTO> itemStream = getItems(type, tags).stream() //

View File

@ -29,7 +29,6 @@ import javax.ws.rs.PUT;
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;
@ -132,17 +131,13 @@ public class TagResource implements RESTResource {
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
CacheControl cc = new CacheControl();
cc.setNoCache(true);
cc.setMustRevalidate(true);
cc.setPrivate(true);
final Locale locale = localeService.getLocale(language);
Stream<EnrichedSemanticTagDTO> tagsStream = semanticTagRegistry.getAll().stream()
.sorted(Comparator.comparing(SemanticTag::getUID))
.map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t)));
return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified).cacheControl(cc).build();
return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
@GET
@ -165,11 +160,6 @@ public class TagResource implements RESTResource {
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
CacheControl cc = new CacheControl();
cc.setNoCache(true);
cc.setMustRevalidate(true);
cc.setPrivate(true);
final Locale locale = localeService.getLocale(language);
String uid = tagId.trim();
@ -178,8 +168,8 @@ public class TagResource implements RESTResource {
Stream<EnrichedSemanticTagDTO> tagsStream = semanticTagRegistry.getSubTree(tag).stream()
.sorted(Comparator.comparing(SemanticTag::getUID))
.map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t)));
return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified).cacheControl(cc)
.build();
return Response.ok(new Stream2JSONInputStream(tagsStream)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
} else {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!");
}

View File

@ -41,7 +41,6 @@ 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;
@ -171,10 +170,10 @@ public class ThingResource implements RESTResource {
private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService;
private final ThingTypeRegistry thingTypeRegistry;
private final RegistryChangedRunnableListener<Thing> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> cacheableListLastModified = null);
() -> lastModified = null);
private @Context @NonNullByDefault({}) UriInfo uriInfo;
private @Nullable Date cacheableListLastModified = null;
private @Nullable Date lastModified = null;
@Activate
public ThingResource( //
@ -317,23 +316,19 @@ public class ThingResource implements RESTResource {
.distinct();
if (staticDataOnly) {
if (cacheableListLastModified != null) {
Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(cacheableListLastModified);
if (lastModified != null) {
Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified);
if (responseBuilder != null) {
// send 304 Not Modified
return responseBuilder.build();
}
} else {
cacheableListLastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
CacheControl cc = new CacheControl();
cc.setNoCache(true);
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();
return Response.ok(new Stream2JSONInputStream(thingStream)).lastModified(lastModified)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
if (summary != null && summary) {

View File

@ -31,7 +31,6 @@ 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;
@ -167,12 +166,8 @@ public class UIResource implements RESTResource {
lastModifiedDates.put(namespace, lastModifiedDate);
}
CacheControl cc = new CacheControl();
cc.setNoCache(true);
cc.setMustRevalidate(true);
cc.setPrivate(true);
return Response.ok(new Stream2JSONInputStream(components)).lastModified(lastModifiedDate).cacheControl(cc)
.build();
return Response.ok(new Stream2JSONInputStream(components)).lastModified(lastModifiedDate)
.cacheControl(RESTConstants.CACHE_CONTROL).build();
}
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.core.io.rest;
import javax.ws.rs.core.CacheControl;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
@ -38,4 +40,11 @@ public class RESTConstants {
* Version 6: extended chart period parameter format (#3863)
*/
public static final String API_VERSION = "6";
public static final CacheControl CACHE_CONTROL = new CacheControl();
static {
CACHE_CONTROL.setNoCache(true);
CACHE_CONTROL.setMustRevalidate(true);
CACHE_CONTROL.setPrivate(true);
}
}

View File

@ -21,7 +21,6 @@ import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
@ -118,12 +117,7 @@ public class SystemInfoResource implements RESTResource, ConfigurationListener {
lastModified = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
}
CacheControl cc = new CacheControl();
cc.setNoCache(true);
cc.setMustRevalidate(true);
cc.setPrivate(true);
final UoMInfoBean bean = new UoMInfoBean(unitProvider);
return Response.ok(bean).lastModified(lastModified).cacheControl(cc).build();
return Response.ok(bean).lastModified(lastModified).cacheControl(RESTConstants.CACHE_CONTROL).build();
}
}