[rest] Add caching for TagResource & De-duplicate code for caching (#3729)

* [rest] Add caching for TagResource
* [core] Add & use RegistryChangedRunnableListener class

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
Florian Hotze 2023-07-25 20:59:39 +02:00 committed by GitHub
parent 79fd459f59
commit e1741cf61d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 108 deletions

View File

@ -74,7 +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.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<Rule> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> cacheableListLastModified = null);
private @Context @NonNullByDefault({}) UriInfo uriInfo;
private @Nullable Date cacheableListLastModified = null;
@ -608,26 +609,4 @@ 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();
}
}
}

View File

@ -57,7 +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.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.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;
@ -183,8 +182,15 @@ public class ItemResource implements RESTResource {
private final MetadataRegistry metadataRegistry;
private final MetadataSelectorMatcher metadataSelectorMatcher;
private final SemanticTagRegistry semanticTagRegistry;
private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener();
private final RegistryChangeListener<Metadata> resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener();
private void resetCacheableListsLastModified() {
this.cacheableListsLastModified.clear();
}
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
this::resetCacheableListsLastModified);
private final RegistryChangedRunnableListener<Metadata> resetLastModifiedMetadataChangeListener = new RegistryChangedRunnableListener<>(
this::resetCacheableListsLastModified);
private Map<@Nullable String, Date> cacheableListsLastModified = new HashMap<>();
@ -990,48 +996,4 @@ 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();
}
}
}

View File

@ -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.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;
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 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.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<SemanticTag> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> lastModified = null);
private @Nullable Date lastModified = null;
// TODO pattern in @Path
@ -93,6 +105,13 @@ public class TagResource implements RESTResource {
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 class TagResource implements RESTResource {
@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<EnrichedSemanticTagDTO> tagsDTO = semanticTagRegistry.getAll().stream()
Stream<EnrichedSemanticTagDTO> 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 class TagResource implements RESTResource {
@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<EnrichedSemanticTagDTO> tagsDTO = semanticTagRegistry.getSubTree(tag).stream()
Stream<EnrichedSemanticTagDTO> 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 class TagResource implements RESTResource {
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

View File

@ -54,7 +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.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<Thing> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> cacheableListLastModified = null);
private @Context @NonNullByDefault({}) UriInfo uriInfo;
private @Nullable Date cacheableListLastModified = null;
@ -890,26 +891,4 @@ 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();
}
}
}

View File

@ -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 <E> type of the element in the registry
*/
@NonNullByDefault
public class RegistryChangedRunnableListener<E> implements RegistryChangeListener<E> {
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();
}
}