diff --git a/access-grant/pom.xml b/access-grant/pom.xml
index 894529afbcf..962acaca090 100644
--- a/access-grant/pom.xml
+++ b/access-grant/pom.xml
@@ -55,6 +55,12 @@
${project.version}
test
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+ test
+
com.inrupt.client
inrupt-client-core
diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java
index 1014700af2e..94779381012 100644
--- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java
+++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java
@@ -23,6 +23,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.inrupt.client.Client;
+import com.inrupt.client.ClientCache;
import com.inrupt.client.ClientProvider;
import com.inrupt.client.Request;
import com.inrupt.client.Response;
@@ -36,6 +37,7 @@
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
+import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
@@ -92,6 +94,7 @@ public class AccessGrantClient {
private static final Set ACCESS_GRANT_TYPES = getAccessGrantTypes();
private final Client client;
+ private final ClientCache metadataCache;
private final JsonService jsonService;
private final AccessGrantConfiguration config;
@@ -107,23 +110,38 @@ public AccessGrantClient(final URI issuer) {
/**
* Create an access grant client.
*
- * @param issuer the issuer
* @param client the client
+ * @param issuer the issuer
*/
public AccessGrantClient(final Client client, final URI issuer) {
- this(client, new AccessGrantConfiguration(issuer));
+ this(client, ServiceProvider.getCacheBuilder().build(100, Duration.ofMinutes(60)),
+ new AccessGrantConfiguration(issuer));
+ }
+
+ /**
+ * Create an access grant client.
+ *
+ * @param client the client
+ * @param issuer the issuer
+ * @param metadataCache the metadata cache
+ */
+ public AccessGrantClient(final Client client, final URI issuer, final ClientCache metadataCache) {
+ this(client, metadataCache, new AccessGrantConfiguration(issuer));
}
/**
* Create an access grant client.
*
* @param client the client
+ * @param metadataCache the metadata cache
* @param config the access grant configuration
*/
// This ctor may be made public at a later point
- private AccessGrantClient(final Client client, final AccessGrantConfiguration config) {
- this.client = Objects.requireNonNull(client);
- this.config = Objects.requireNonNull(config);
+ private AccessGrantClient(final Client client, final ClientCache metadataCache,
+ final AccessGrantConfiguration config) {
+ this.client = Objects.requireNonNull(client, "client may not be null!");
+ this.config = Objects.requireNonNull(config, "config may not be null!");
+ this.metadataCache = Objects.requireNonNull(metadataCache, "metadataCache may not be null!");
this.jsonService = ServiceProvider.getJsonService();
}
@@ -135,7 +153,7 @@ private AccessGrantClient(final Client client, final AccessGrantConfiguration co
*/
public AccessGrantClient session(final Session session) {
Objects.requireNonNull(session, "Session may not be null!");
- return new AccessGrantClient(client.session(session), config);
+ return new AccessGrantClient(client.session(session), metadataCache, config);
}
/**
@@ -346,6 +364,11 @@ List processQueryResponse(final InputStream input, final Set v1Metadata() {
final URI uri = URIBuilder.newBuilder(config.getIssuer()).path(".well-known/vc-configuration").build();
+ final Metadata cached = metadataCache.get(uri);
+ if (cached != null) {
+ return CompletableFuture.completedFuture(cached);
+ }
+
final Request req = Request.newBuilder(uri).header("Accept", APPLICATION_JSON).build();
return client.send(req, Response.BodyHandlers.ofInputStream())
.thenApply(res -> {
@@ -369,6 +392,7 @@ CompletionStage v1Metadata() {
m.issueEndpoint = asUri(metadata.get("issuerService"));
m.verifyEndpoint = asUri(metadata.get("verifierService"));
m.statusEndpoint = asUri(metadata.get("statusService"));
+ metadataCache.put(uri, m);
return m;
});
}
diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java
index afd8b5425cc..21b3ea6d8d1 100644
--- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java
+++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java
@@ -65,11 +65,13 @@ class AccessGrantClientTest {
private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
private static final MockAccessGrantServer mockServer = new MockAccessGrantServer();
+ private static AccessGrantClient agClient;
private static URI baseUri;
@BeforeAll
static void setup() {
baseUri = URI.create(mockServer.start());
+ agClient = new AccessGrantClient(baseUri);
}
@AfterAll
@@ -93,8 +95,7 @@ void testFetch1() {
claims.put("azp", AZP);
final String token = generateIdToken(claims);
final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-1").build();
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final AccessGrant grant = client.fetch(uri).toCompletableFuture().join();
assertEquals(uri, grant.getIdentifier());
@@ -116,8 +117,7 @@ void testFetch2() {
claims.put("azp", AZP);
final String token = generateIdToken(claims);
final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-2").build();
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final AccessGrant grant = client.fetch(uri).toCompletableFuture().join();
assertEquals(uri, grant.getIdentifier());
@@ -143,8 +143,7 @@ void testFetch6() {
claims.put("azp", AZP);
final String token = generateIdToken(claims);
final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-6").build();
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final AccessGrant grant = client.fetch(uri).toCompletableFuture().join();
assertEquals(uri, grant.getIdentifier());
@@ -168,8 +167,7 @@ void testNotAccessGrant() {
claims.put("azp", AZP);
final String token = generateIdToken(claims);
final URI uri = URIBuilder.newBuilder(baseUri).path("vc-3").build();
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final CompletionException err = assertThrows(CompletionException.class,
client.fetch(uri).toCompletableFuture()::join);
}
@@ -177,9 +175,8 @@ void testNotAccessGrant() {
@Test
void testFetchInvalid() {
final URI uri = URIBuilder.newBuilder(baseUri).path(".well-known/vc-configuration").build();
- final AccessGrantClient client = new AccessGrantClient(baseUri);
final CompletionException err = assertThrows(CompletionException.class,
- client.fetch(uri).toCompletableFuture()::join);
+ agClient.fetch(uri).toCompletableFuture()::join);
assertTrue(err.getCause() instanceof AccessGrantException);
}
@@ -187,16 +184,15 @@ void testFetchInvalid() {
@Test
void testFetchNotFound() {
final URI uri = URIBuilder.newBuilder(baseUri).path("not-found").build();
- final AccessGrantClient client = new AccessGrantClient(uri);
final CompletionException err1 = assertThrows(CompletionException.class,
- client.fetch(uri).toCompletableFuture()::join);
+ agClient.fetch(uri).toCompletableFuture()::join);
assertTrue(err1.getCause() instanceof AccessGrantException);
final URI agent = URI.create("https://id.test/agent");
final CompletionException err2 = assertThrows(CompletionException.class,
- client.issue(ACCESS_GRANT, agent, Collections.emptySet(), Collections.emptySet(),
+ agClient.issue(ACCESS_GRANT, agent, Collections.emptySet(), Collections.emptySet(),
Collections.emptySet(), Instant.now()).toCompletableFuture()::join);
assertTrue(err2.getCause() instanceof AccessGrantException);
}
@@ -209,8 +205,7 @@ void testIssueGrant() {
claims.put("iss", ISS);
claims.put("azp", AZP);
final String token = generateIdToken(claims);
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final URI agent = URI.create("https://id.test/agent");
final Instant expiration = Instant.parse("2022-08-27T12:00:00Z");
@@ -238,8 +233,7 @@ void testIssueRequest() {
claims.put("iss", ISS);
claims.put("azp", AZP);
final String token = generateIdToken(claims);
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final URI agent = URI.create("https://id.test/agent");
final Instant expiration = Instant.parse("2022-08-27T12:00:00Z");
@@ -261,8 +255,6 @@ void testIssueRequest() {
@Test
void testIssueNoAuth() {
- final AccessGrantClient client = new AccessGrantClient(baseUri);
-
final URI agent = URI.create("https://id.test/agent");
final Instant expiration = Instant.parse("2022-08-27T12:00:00Z");
final Set modes = new HashSet<>(Arrays.asList("Read", "Append"));
@@ -270,7 +262,7 @@ void testIssueNoAuth() {
final Set resources = Collections.singleton(URI.create("https://storage.test/data/"));
final CompletionException err = assertThrows(CompletionException.class, () ->
- client.issue(ACCESS_GRANT, agent, resources, modes, purposes, expiration)
+ agClient.issue(ACCESS_GRANT, agent, resources, modes, purposes, expiration)
.toCompletableFuture().join());
assertTrue(err.getCause() instanceof AccessGrantException);
}
@@ -283,8 +275,7 @@ void testIssueOther() {
claims.put("iss", ISS);
claims.put("azp", AZP);
final String token = generateIdToken(claims);
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final URI agent = URI.create("https://id.test/agent");
final Instant expiration = Instant.parse("2022-08-27T12:00:00Z");
@@ -306,8 +297,7 @@ void testQueryGrant() {
claims.put("iss", ISS);
claims.put("azp", AZP);
final String token = generateIdToken(claims);
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final List grants = client.query(URI.create("SolidAccessGrant"), null,
URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"), "Read")
@@ -323,8 +313,7 @@ void testQueryRequest() {
claims.put("iss", ISS);
claims.put("azp", AZP);
final String token = generateIdToken(claims);
- final AccessGrantClient client = new AccessGrantClient(baseUri)
- .session(OpenIdSession.ofIdToken(token));
+ final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));
final List grants = client.query(URI.create("SolidAccessRequest"), null,
URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), "Read")
@@ -334,10 +323,8 @@ void testQueryRequest() {
@Test
void testQueryInvalidAuth() {
- final AccessGrantClient client = new AccessGrantClient(baseUri);
-
final CompletionException err = assertThrows(CompletionException.class,
- client.query(URI.create("SolidAccessGrant"), null, null, null).toCompletableFuture()::join);
+ agClient.query(URI.create("SolidAccessGrant"), null, null, null).toCompletableFuture()::join);
assertTrue(err.getCause() instanceof AccessGrantException);
}
diff --git a/api/src/main/java/com/inrupt/client/ClientCache.java b/api/src/main/java/com/inrupt/client/ClientCache.java
new file mode 100644
index 00000000000..4891e5f1046
--- /dev/null
+++ b/api/src/main/java/com/inrupt/client/ClientCache.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client;
+
+/**
+ * A generic caching abstraction for use in the Inrupt Client Libraries.
+ *
+ * @param the key type
+ * @param the value type
+ */
+public interface ClientCache {
+
+ /**
+ * Retrieve a cached value.
+ *
+ * @param key the key
+ * @return the cached value, may be {@code null} if not present
+ */
+ U get(T key);
+
+ /**
+ * Set a cached value.
+ *
+ * @param key the key, not {@code null}
+ * @param value the value, not {@code null}
+ */
+ void put(T key, U value);
+
+ /**
+ * Invalidate a single cached value.
+ *
+ * @param key the key, not {@code null}
+ */
+ void invalidate(T key);
+
+ /**
+ * Invalidate all values in the cache.
+ */
+ void invalidateAll();
+}
diff --git a/api/src/main/java/com/inrupt/client/spi/CacheBuilderService.java b/api/src/main/java/com/inrupt/client/spi/CacheBuilderService.java
new file mode 100644
index 00000000000..1b95f3076c6
--- /dev/null
+++ b/api/src/main/java/com/inrupt/client/spi/CacheBuilderService.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.spi;
+
+import com.inrupt.client.ClientCache;
+
+import java.time.Duration;
+
+/**
+ * A cache builder abstraction for use with different cache implementations.
+ */
+public interface CacheBuilderService {
+
+ /**
+ * Build a cache.
+ *
+ * @param maximumSize the maximum cache size
+ * @param expiration the duration after which items should expire from the cache
+ * @param the key type
+ * @param the value type
+ * @return the cache
+ */
+ ClientCache build(int maximumSize, Duration expiration);
+
+}
diff --git a/api/src/main/java/com/inrupt/client/spi/NoopCache.java b/api/src/main/java/com/inrupt/client/spi/NoopCache.java
new file mode 100644
index 00000000000..8777bb8f6d9
--- /dev/null
+++ b/api/src/main/java/com/inrupt/client/spi/NoopCache.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.spi;
+
+import com.inrupt.client.ClientCache;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * A no-op cache implementation.
+ *
+ * @param the key type
+ * @param the value type
+ */
+class NoopCache implements ClientCache {
+
+ @Override
+ public U get(final T key) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ return null;
+ }
+
+ @Override
+ public void put(final T key, final U value) {
+ /* no-op */
+ Objects.requireNonNull(key, "cache key may not be null!");
+ Objects.requireNonNull(value, "cache value may not be null!");
+ }
+
+ @Override
+ public void invalidate(final T key) {
+ /* no-op */
+ Objects.requireNonNull(key, "cache key may not be null!");
+ }
+
+ @Override
+ public void invalidateAll() {
+ /* no-op */
+ }
+
+ public static class NoopCacheBuilder implements CacheBuilderService {
+ @Override
+ public ClientCache build(final int size, final Duration duration) {
+ return new NoopCache();
+ }
+ }
+}
diff --git a/api/src/main/java/com/inrupt/client/spi/ServiceProvider.java b/api/src/main/java/com/inrupt/client/spi/ServiceProvider.java
index dc4303422d6..612aa935b45 100644
--- a/api/src/main/java/com/inrupt/client/spi/ServiceProvider.java
+++ b/api/src/main/java/com/inrupt/client/spi/ServiceProvider.java
@@ -33,6 +33,7 @@ public final class ServiceProvider {
private static HttpService httpService;
private static DpopService dpopService;
private static HeaderParser headerParser;
+ private static CacheBuilderService cacheBuilder;
/**
* Get the {@link JsonService} for this application.
@@ -122,6 +123,25 @@ public static HeaderParser getHeaderParser() {
return headerParser;
}
+ /**
+ * Get the {@link CacheBuilderService} for this application.
+ *
+ * @return a service capable of building a cache.
+ */
+ public static CacheBuilderService getCacheBuilder() {
+ if (cacheBuilder == null) {
+ synchronized (ServiceProvider.class) {
+ if (cacheBuilder != null) {
+ return cacheBuilder;
+ }
+ final Iterator iter = ServiceLoader.load(CacheBuilderService.class,
+ ServiceProvider.class.getClassLoader()).iterator();
+ cacheBuilder = iter.hasNext() ? iter.next() : new NoopCache.NoopCacheBuilder();
+ }
+ }
+ return cacheBuilder;
+ }
+
static T loadSpi(final Class clazz, final ClassLoader cl) {
final ServiceLoader loader = ServiceLoader.load(clazz, cl);
final Iterator iterator = loader.iterator();
diff --git a/api/src/test/java/com/inrupt/client/spi/NoopCacheTest.java b/api/src/test/java/com/inrupt/client/spi/NoopCacheTest.java
new file mode 100644
index 00000000000..e1e45f5f6fb
--- /dev/null
+++ b/api/src/test/java/com/inrupt/client/spi/NoopCacheTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.spi;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.inrupt.client.ClientCache;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+class NoopCacheTest {
+
+ @Test
+ void testServiceLoader() {
+
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ assertTrue(svc instanceof NoopCache.NoopCacheBuilder);
+ }
+
+ @Test
+ void testCacheBuilder() {
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ final ClientCache cache = svc.build(10, Duration.ofMinutes(5));
+
+ cache.put("one", 1);
+ cache.put("two", 2);
+ cache.put("three", 3);
+
+ assertNull(cache.get("zero"));
+ assertNull(cache.get("one"));
+ assertNull(cache.get("two"));
+ assertNull(cache.get("three"));
+
+ cache.invalidate("one");
+ assertNull(cache.get("one"));
+ assertNull(cache.get("two"));
+ assertNull(cache.get("three"));
+
+ cache.invalidateAll();
+ assertNull(cache.get("one"));
+ assertNull(cache.get("two"));
+ assertNull(cache.get("three"));
+ }
+}
diff --git a/archetypes/java/src/main/resources/archetype-resources/pom.xml b/archetypes/java/src/main/resources/archetype-resources/pom.xml
index f111d981eef..935a1abb214 100644
--- a/archetypes/java/src/main/resources/archetype-resources/pom.xml
+++ b/archetypes/java/src/main/resources/archetype-resources/pom.xml
@@ -44,6 +44,10 @@
com.inrupt.client
inrupt-client-solid
+
+ com.inrupt.client
+ inrupt-client-caffeine
+
com.inrupt.client
inrupt-client-core
@@ -64,6 +68,10 @@
com.inrupt.client
inrupt-client-openid
+
+ com.inrupt.client
+ inrupt-client-uma
+
com.inrupt.client
inrupt-client-vocabulary
diff --git a/bom/pom.xml b/bom/pom.xml
index 713f615fcb3..757908fe20e 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -28,11 +28,21 @@
inrupt-client-accessgrant
${project.version}
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+
com.inrupt.client
inrupt-client-core
${project.version}
+
+ com.inrupt.client
+ inrupt-client-guava
+ ${project.version}
+
com.inrupt.client
inrupt-client-okhttp
diff --git a/caffeine/pom.xml b/caffeine/pom.xml
new file mode 100644
index 00000000000..c8f1c456830
--- /dev/null
+++ b/caffeine/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.inrupt.client
+ inrupt-client
+ 1.0.0-SNAPSHOT
+
+
+ inrupt-client-caffeine
+ Inrupt Java Client Libraries - Caffeine Cache
+
+ Caffeine cache integration for the Inrupt Java Client Libraries.
+
+
+
+
+ com.inrupt.client
+ inrupt-client-api
+ ${project.version}
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
diff --git a/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCache.java b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCache.java
new file mode 100644
index 00000000000..0000b483052
--- /dev/null
+++ b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCache.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.caffeine;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.inrupt.client.ClientCache;
+
+import java.util.Objects;
+
+/**
+ * A cache implementation using Caffeine.
+ *
+ * @param the key type
+ * @param the value type
+ */
+public class CaffeineCache implements ClientCache {
+
+ private final Cache cache;
+
+ /**
+ * Wrap an existing caffeine {@link Cache}.
+ *
+ * @param cache the caffeine cache
+ */
+ public CaffeineCache(final Cache cache) {
+ this.cache = Objects.requireNonNull(cache, "cache may not be null!");
+ }
+
+ @Override
+ public U get(final T key) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ return cache.getIfPresent(key);
+ }
+
+ @Override
+ public void put(final T key, final U value) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ Objects.requireNonNull(value, "cache value may not be null!");
+ cache.put(key, value);
+ }
+
+ @Override
+ public void invalidate(final T key) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ cache.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ cache.invalidateAll();
+ }
+}
diff --git a/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java
new file mode 100644
index 00000000000..a5a5b5cdae3
--- /dev/null
+++ b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.caffeine;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.inrupt.client.ClientCache;
+import com.inrupt.client.spi.CacheBuilderService;
+
+import java.time.Duration;
+
+/**
+ * A {@link CacheBuilderService} using a Caffeine-based cache.
+ */
+public class CaffeineCacheBuilder implements CacheBuilderService {
+
+ @Override
+ public ClientCache build(final int maximumSize, final Duration duration) {
+ return ofCache(Caffeine.newBuilder()
+ .maximumSize(maximumSize)
+ .expireAfterWrite(duration)
+ .build());
+ }
+
+ /**
+ * Create a {@link ClientCache} directly from an existing Caffeine {@link Cache}.
+ *
+ * @param cache the pre-built cache
+ * @param the key type
+ * @param the value type
+ * @return a cache suitable for use in the Inrupt Client libraries
+ */
+ public static ClientCache ofCache(final Cache cache) {
+ return new CaffeineCache(cache);
+ }
+}
+
+
diff --git a/caffeine/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService b/caffeine/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService
new file mode 100644
index 00000000000..e95ebdfe22f
--- /dev/null
+++ b/caffeine/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService
@@ -0,0 +1 @@
+com.inrupt.client.caffeine.CaffeineCacheBuilder
diff --git a/caffeine/src/test/java/com/inrupt/client/caffeine/CaffeineCacheTest.java b/caffeine/src/test/java/com/inrupt/client/caffeine/CaffeineCacheTest.java
new file mode 100644
index 00000000000..3e1ef6caea9
--- /dev/null
+++ b/caffeine/src/test/java/com/inrupt/client/caffeine/CaffeineCacheTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.caffeine;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.inrupt.client.ClientCache;
+import com.inrupt.client.spi.CacheBuilderService;
+import com.inrupt.client.spi.ServiceProvider;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+class CaffeineCacheTest {
+
+ @Test
+ void testServiceLoader() {
+
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ assertTrue(svc instanceof CaffeineCacheBuilder);
+ }
+
+ @Test
+ void testCacheBuilder() {
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ final ClientCache cache = svc.build(10, Duration.ofMinutes(5));
+
+ cache.put("one", 1);
+ cache.put("two", 2);
+ cache.put("three", 3);
+
+ assertNull(cache.get("zero"));
+ assertEquals(1, cache.get("one"));
+ assertEquals(2, cache.get("two"));
+ assertEquals(3, cache.get("three"));
+
+ cache.invalidate("one");
+ assertNull(cache.get("one"));
+ assertEquals(2, cache.get("two"));
+ assertEquals(3, cache.get("three"));
+
+ cache.invalidateAll();
+ assertNull(cache.get("one"));
+ assertNull(cache.get("two"));
+ assertNull(cache.get("three"));
+ }
+}
diff --git a/examples/cli/pom.xml b/examples/cli/pom.xml
index 8782181813f..1d5cff3fbab 100644
--- a/examples/cli/pom.xml
+++ b/examples/cli/pom.xml
@@ -54,6 +54,11 @@
inrupt-client-webid
${project.version}
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+
com.inrupt.client
inrupt-client-jena
diff --git a/examples/springboot/pom.xml b/examples/springboot/pom.xml
index 862c1972690..54a5aa8ce20 100644
--- a/examples/springboot/pom.xml
+++ b/examples/springboot/pom.xml
@@ -60,11 +60,21 @@
inrupt-client-core
${project.version}
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+
com.inrupt.client
inrupt-client-openid
${project.version}
+
+ com.inrupt.client
+ inrupt-client-uma
+ ${project.version}
+
com.inrupt.client
inrupt-client-solid
diff --git a/examples/webapp/pom.xml b/examples/webapp/pom.xml
index c6ceb6a390a..23b9c581489 100644
--- a/examples/webapp/pom.xml
+++ b/examples/webapp/pom.xml
@@ -50,6 +50,11 @@
inrupt-client-webid
${project.version}
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+
com.inrupt.client
inrupt-client-jena
diff --git a/guava/pom.xml b/guava/pom.xml
new file mode 100644
index 00000000000..d6a9a5c6d51
--- /dev/null
+++ b/guava/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.inrupt.client
+ inrupt-client
+ 1.0.0-SNAPSHOT
+
+
+ inrupt-client-guava
+ Inrupt Java Client Libraries - Guava Cache
+
+ Guava cache integration for the Inrupt Java Client Libraries.
+
+
+
+
+ com.inrupt.client
+ inrupt-client-api
+ ${project.version}
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
diff --git a/guava/src/main/java/com/inrupt/client/guava/GuavaCache.java b/guava/src/main/java/com/inrupt/client/guava/GuavaCache.java
new file mode 100644
index 00000000000..06725196d03
--- /dev/null
+++ b/guava/src/main/java/com/inrupt/client/guava/GuavaCache.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.guava;
+
+import com.google.common.cache.Cache;
+import com.inrupt.client.ClientCache;
+
+import java.util.Objects;
+
+/**
+ * A cache implementation using Guava.
+ *
+ * @param the key type
+ * @param the value type
+ */
+public class GuavaCache implements ClientCache {
+
+ private final Cache cache;
+
+ /**
+ * Wrap an existing guava {@link Cache}.
+ *
+ * @param cache the guava cache
+ */
+ public GuavaCache(final Cache cache) {
+ this.cache = Objects.requireNonNull(cache, "cache may not be null!");
+ }
+
+ @Override
+ public U get(final T key) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ return cache.getIfPresent(key);
+ }
+
+ @Override
+ public void put(final T key, final U value) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ Objects.requireNonNull(value, "cache value may not be null!");
+ cache.put(key, value);
+ }
+
+ @Override
+ public void invalidate(final T key) {
+ Objects.requireNonNull(key, "cache key may not be null!");
+ cache.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ cache.invalidateAll();
+ }
+}
diff --git a/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java b/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java
new file mode 100644
index 00000000000..e5b07eb5e1b
--- /dev/null
+++ b/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.guava;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.inrupt.client.ClientCache;
+import com.inrupt.client.spi.CacheBuilderService;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link CacheBuilderService} using a Guava-based cache.
+ */
+public class GuavaCacheBuilder implements CacheBuilderService {
+
+ @Override
+ public ClientCache build(final int maximumSize, final Duration duration) {
+ return ofCache(CacheBuilder.newBuilder()
+ .maximumSize(maximumSize)
+ .expireAfterWrite(duration.getSeconds(), TimeUnit.SECONDS)
+ .build());
+ }
+
+ /**
+ * Create a {@link ClientCache} directly from an existing Guava {@link Cache}.
+ *
+ * @param cache the pre-built cache
+ * @param the key type
+ * @param the value type
+ * @return a cache suitable for use in the Inrupt Client libraries
+ */
+ public static ClientCache ofCache(final Cache cache) {
+ return new GuavaCache(cache);
+ }
+}
+
+
diff --git a/guava/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService b/guava/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService
new file mode 100644
index 00000000000..5e215ea6c88
--- /dev/null
+++ b/guava/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService
@@ -0,0 +1 @@
+com.inrupt.client.guava.GuavaCacheBuilder
diff --git a/guava/src/test/java/com/inrupt/client/guava/GuavaCacheTest.java b/guava/src/test/java/com/inrupt/client/guava/GuavaCacheTest.java
new file mode 100644
index 00000000000..0d59ccc8026
--- /dev/null
+++ b/guava/src/test/java/com/inrupt/client/guava/GuavaCacheTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.guava;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.inrupt.client.ClientCache;
+import com.inrupt.client.spi.CacheBuilderService;
+import com.inrupt.client.spi.ServiceProvider;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+class GuavaCacheTest {
+
+ @Test
+ void testServiceLoader() {
+
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ assertTrue(svc instanceof GuavaCacheBuilder);
+ }
+
+ @Test
+ void testCacheBuilder() {
+ final CacheBuilderService svc = ServiceProvider.getCacheBuilder();
+ final ClientCache cache = svc.build(10, Duration.ofMinutes(5));
+
+ cache.put("one", 1);
+ cache.put("two", 2);
+ cache.put("three", 3);
+
+ assertNull(cache.get("zero"));
+ assertEquals(1, cache.get("one"));
+ assertEquals(2, cache.get("two"));
+ assertEquals(3, cache.get("three"));
+
+ cache.invalidate("one");
+ assertNull(cache.get("one"));
+ assertEquals(2, cache.get("two"));
+ assertEquals(3, cache.get("three"));
+
+ cache.invalidateAll();
+ assertNull(cache.get("one"));
+ assertNull(cache.get("two"));
+ assertNull(cache.get("three"));
+ }
+}
diff --git a/openid/pom.xml b/openid/pom.xml
index 00c23ac3ea6..c0d15d70861 100644
--- a/openid/pom.xml
+++ b/openid/pom.xml
@@ -53,6 +53,12 @@
${wiremock.version}
test
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+ test
+
com.inrupt.client
inrupt-client-httpclient
diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java
index 965ff8ed65b..41147790f28 100644
--- a/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java
+++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java
@@ -34,6 +34,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
+import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
@@ -41,6 +42,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -60,6 +62,7 @@ public class OpenIdProvider {
private final URI issuer;
private final HttpService httpClient;
private final JsonService jsonService;
+ private final ClientCache metadataCache;
private final DPoP dpop;
/**
@@ -80,8 +83,22 @@ public OpenIdProvider(final URI issuer, final DPoP dpop) {
* @param httpClient an HTTP client
*/
public OpenIdProvider(final URI issuer, final DPoP dpop, final HttpService httpClient) {
- this.issuer = Objects.requireNonNull(issuer);
- this.httpClient = Objects.requireNonNull(httpClient);
+ this(issuer, dpop, httpClient, ServiceProvider.getCacheBuilder().build(100, Duration.ofMinutes(60)));
+ }
+
+ /**
+ * Create an OpenID Provider client.
+ *
+ * @param issuer the OpenID provider issuer
+ * @param dpop the DPoP manager
+ * @param httpClient an HTTP client
+ * @param metadataCache an OpenID Metadata cache
+ */
+ public OpenIdProvider(final URI issuer, final DPoP dpop, final HttpService httpClient,
+ final ClientCache metadataCache) {
+ this.issuer = Objects.requireNonNull(issuer, "issuer may not be null!");
+ this.httpClient = Objects.requireNonNull(httpClient, "httpClient may not be null!");
+ this.metadataCache = Objects.requireNonNull(metadataCache, "metadataCache may not be null!");
this.jsonService = ServiceProvider.getJsonService();
this.dpop = dpop;
}
@@ -92,14 +109,21 @@ public OpenIdProvider(final URI issuer, final DPoP dpop, final HttpService httpC
* @return the next stage of completion, containing the OpenID Provider's metadata resource
*/
public CompletionStage metadata() {
- // consider caching this response
+ final URI uri = getMetadataUrl();
+ final Metadata m = metadataCache.get(uri);
+ if (m != null) {
+ return CompletableFuture.completedFuture(m);
+ }
+
final Request req = Request.newBuilder(getMetadataUrl()).header("Accept", "application/json").build();
return httpClient.send(req, Response.BodyHandlers.ofInputStream())
.thenApply(res -> {
try {
final int httpStatus = res.statusCode();
if (httpStatus >= 200 && httpStatus < 300) {
- return jsonService.fromJson(res.body(), Metadata.class);
+ final Metadata discovery = jsonService.fromJson(res.body(), Metadata.class);
+ metadataCache.put(uri, discovery);
+ return discovery;
}
throw new OpenIdException(
"Unexpected error while fetching the OpenID metadata resource.",
diff --git a/pom.xml b/pom.xml
index 4c9a4fe5dd6..a7410c45099 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,11 +21,12 @@
4.12.0
- 3.1.2
+ 3.1.5
1.15
1.5.0
2.11.0
0.5.0
+ 31.1-jre
2.14.2
1.1.6
4.7.0
@@ -86,8 +87,10 @@
access-grant
api
bom
+ caffeine
core
examples
+ guava
httpclient
integration
jackson
diff --git a/reports/pom.xml b/reports/pom.xml
index f8708c57ae8..2bb6e04eb53 100644
--- a/reports/pom.xml
+++ b/reports/pom.xml
@@ -22,11 +22,21 @@
inrupt-client-accessgrant
${project.version}
+
+ com.inrupt.client
+ inrupt-client-caffeine
+ ${project.version}
+
com.inrupt.client
inrupt-client-core
${project.version}
+
+ com.inrupt.client
+ inrupt-client-guava
+ ${project.version}
+
com.inrupt.client
inrupt-client-jackson
diff --git a/uma/pom.xml b/uma/pom.xml
index d390689de63..aa7ed129593 100644
--- a/uma/pom.xml
+++ b/uma/pom.xml
@@ -55,6 +55,12 @@
${wiremock.version}
test
+
+ com.inrupt.client
+ inrupt-client-guava
+ ${project.version}
+ test
+
com.inrupt.client
inrupt-client-httpclient
diff --git a/uma/src/main/java/com/inrupt/client/uma/UmaClient.java b/uma/src/main/java/com/inrupt/client/uma/UmaClient.java
index 3eb478fcae7..1b061c5c163 100644
--- a/uma/src/main/java/com/inrupt/client/uma/UmaClient.java
+++ b/uma/src/main/java/com/inrupt/client/uma/UmaClient.java
@@ -33,6 +33,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
+import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -74,7 +75,7 @@ public class UmaClient {
private static final String NEED_INFO = "need_info";
private static final String REQUEST_DENIED = "request_denied";
- // TODO add metadata cache
+ private final ClientCache metadataCache;
private final HttpService httpClient;
private final JsonService jsonService;
private final int maxIterations;
@@ -97,11 +98,25 @@ public UmaClient(final int maxIterations) {
* @param maxIterations the maximum number of claims gathering stages
*/
public UmaClient(final HttpService httpClient, final int maxIterations) {
- this.httpClient = httpClient;
+ this(httpClient, ServiceProvider.getCacheBuilder().build(100, Duration.ofMinutes(60)), maxIterations);
+ }
+
+ /**
+ * Create an UMA client using an externally-configured HTTP client and cache.
+ *
+ * @param httpClient the externally configured HTTP client
+ * @param metadataCache the externally configured metadata cache
+ * @param maxIterations the maximum number of claims gathering stages
+ */
+ public UmaClient(final HttpService httpClient, final ClientCache metadataCache,
+ final int maxIterations) {
+ this.httpClient = Objects.requireNonNull(httpClient, "httpClient may not be null!");
+ this.metadataCache = Objects.requireNonNull(metadataCache, "metadataCache may not be null!");
this.maxIterations = maxIterations;
this.jsonService = ServiceProvider.getJsonService();
}
+
/**
* Fetch the UMA metadata resource.
*
@@ -109,9 +124,15 @@ public UmaClient(final HttpService httpClient, final int maxIterations) {
* @return the next stage of completion, containing the authorization server discovery metadata
*/
public CompletionStage metadata(final URI authorizationServer) {
- final Request req = Request.newBuilder(getMetadataUrl(authorizationServer)).header(ACCEPT, JSON).build();
+ final URI uri = getMetadataUrl(authorizationServer);
+ final Metadata m = metadataCache.get(uri);
+ if (m != null) {
+ return CompletableFuture.completedFuture(m);
+ }
+
+ final Request req = Request.newBuilder(uri).header(ACCEPT, JSON).build();
return httpClient.send(req, Response.BodyHandlers.ofInputStream())
- .thenApply(this::processMetadataResponse);
+ .thenApply(res -> processMetadataResponse(uri, res));
}
/**
@@ -237,10 +258,13 @@ private static Request.BodyPublisher ofFormData(final Map data)
}
- private Metadata processMetadataResponse(final Response response) {
+ private Metadata processMetadataResponse(final URI uri, final Response response) {
if (response.statusCode() == SUCCESS) {
try {
- return jsonService.fromJson(response.body(), Metadata.class);
+ final Metadata metadata = jsonService.fromJson(response.body(), Metadata.class);
+ metadataCache.put(uri, metadata);
+ return metadata;
+
} catch (final IOException ex) {
throw new UmaException("Error while processing UMA metadata response", ex);
}
diff --git a/uma/src/test/java/com/inrupt/client/uma/UmaClientTest.java b/uma/src/test/java/com/inrupt/client/uma/UmaClientTest.java
index fdd394ec68f..af89c07f10d 100644
--- a/uma/src/test/java/com/inrupt/client/uma/UmaClientTest.java
+++ b/uma/src/test/java/com/inrupt/client/uma/UmaClientTest.java
@@ -42,6 +42,7 @@
class UmaClientTest {
+ private static final UmaClient client = new UmaClient();
private static final MockAuthorizationServer as = new MockAuthorizationServer();
private static final Map config = new HashMap<>();
private static final URI ID_TOKEN_CLAIM_TOKEN_FORMAT =
@@ -60,7 +61,6 @@ static void teardown() {
@Test
void testMetadataAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final Metadata metadata = client.metadata(asUri).toCompletableFuture().join();
checkMetadata(metadata);
}
@@ -68,7 +68,6 @@ void testMetadataAsync() {
@Test
void testMetadataNotFoundAsync() {
final URI asUri = URI.create(config.get("as_uri") + "/not-found");
- final UmaClient client = new UmaClient();
final CompletionException err = assertThrows(CompletionException.class,
client.metadata(asUri).toCompletableFuture()::join);
assertTrue(err.getCause() instanceof UmaException);
@@ -77,7 +76,6 @@ void testMetadataNotFoundAsync() {
@Test
void testMetadataMalformedAsync() {
final URI asUri = URI.create(config.get("as_uri") + "/malformed");
- final UmaClient client = new UmaClient();
final CompletionException err = assertThrows(CompletionException.class,
client.metadata(asUri).toCompletableFuture()::join);
assertTrue(err.getCause() instanceof UmaException);
@@ -86,7 +84,6 @@ void testMetadataMalformedAsync() {
@Test
void testSimpleTokenNegotiationInvalidTicketAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-invalid-grant";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
@@ -104,7 +101,6 @@ void testSimpleTokenNegotiationInvalidTicketAsync() {
@Test
void testSimpleTokenNegotiationRequestDeniedAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-request-denied";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
@@ -123,7 +119,6 @@ void testSimpleTokenNegotiationRequestDeniedAsync() {
@MethodSource
void testSimpleTokenErrorAsync(final String ticket) {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
final CompletionException err =
@@ -145,7 +140,6 @@ private static Stream testSimpleTokenErrorAsync() {
@Test
void testSimpleTokenInvalidScopeAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-invalid-scope";
final TokenRequest req = new TokenRequest(ticket, null, null, null, Arrays.asList("invalid-scope"));
@@ -163,7 +157,6 @@ void testSimpleTokenInvalidScopeAsync() {
@Test
void testSimpleTokenNegotiationAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-12345";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
@@ -179,7 +172,6 @@ void testSimpleTokenNegotiationAsync() {
@Test
void testTokenNegotiationMissingResponseTicketAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-need-info-no-response-ticket";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
@@ -197,7 +189,6 @@ void testTokenNegotiationMissingResponseTicketAsync() {
@Test
void testTokenNegotiationNullResponseAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String ticket = "ticket-need-info-with-ticket";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);
@@ -214,7 +205,6 @@ void testTokenNegotiationNullResponseAsync() {
@Test
void testTokenNegotiationOidcMapperAsync() {
final URI asUri = URI.create(config.get("as_uri"));
- final UmaClient client = new UmaClient();
final String idToken = "oidc-id-token";
final String ticket = "ticket-need-info-oidc-requirement";
final TokenRequest req = new TokenRequest(ticket, null, null, null, null);