"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:
Yannick Schaus
2023-06-15 20:16:14 +02:00
committed by GitHub
parent 885a8548c8
commit 6e83d3f8de
5 changed files with 322 additions and 23 deletions
@@ -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));