Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Mar 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
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;
Expand Down Expand Up @@ -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( //
Expand Down Expand Up @@ -174,26 +173,22 @@ public Response get(@Context SecurityContext securityContext, @Context Request r
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
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;
Expand All @@ -37,6 +40,7 @@
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;
Expand All @@ -45,6 +49,7 @@
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;
Expand All @@ -59,6 +64,7 @@
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;
Expand Down Expand Up @@ -106,13 +112,14 @@
@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<>();
Expand All @@ -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
Expand All @@ -142,30 +151,59 @@ public AddonResource(final @Reference EventPublisher eventPublisher, final @Refe
@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();
}
}

Expand All @@ -174,12 +212,23 @@ public Response getAddon(
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
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;
Expand Down Expand Up @@ -182,16 +181,12 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
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(//
Expand Down Expand Up @@ -250,17 +245,14 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead
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(null, null).stream() //
Expand All @@ -270,12 +262,8 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead
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() //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
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;
Expand Down Expand Up @@ -132,17 +131,13 @@ public Response getTags(final @Context Request request, final @Context UriInfo u
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
Expand All @@ -165,11 +160,6 @@ public Response getTagAndSubTags(final @Context Request request,
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();

Expand All @@ -178,8 +168,8 @@ public Response getTagAndSubTags(final @Context Request request,
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!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
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;
Expand Down Expand Up @@ -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( //
Expand Down Expand Up @@ -317,23 +316,19 @@ public Response getAll(@Context Request request,
.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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
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;
Expand Down Expand Up @@ -167,12 +166,8 @@ public Response getAllComponents(@Context Request request, @PathParam("namespace
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();
}
}

Expand Down