From c57ae020342d75a128c64c6233462b9a5bf22224 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 08:37:06 -0400 Subject: [PATCH 1/7] JCL-279: Cache abstraction with Guava and Caffeine implementations --- .../java/com/inrupt/client/ClientCache.java | 58 +++++++++++++++ .../client/spi/CacheBuilderService.java | 43 ++++++++++++ .../java/com/inrupt/client/spi/NoopCache.java | 66 +++++++++++++++++ .../inrupt/client/spi/ServiceProvider.java | 20 ++++++ .../com/inrupt/client/spi/NoopCacheTest.java | 64 +++++++++++++++++ bom/pom.xml | 10 +++ caffeine/pom.xml | 59 ++++++++++++++++ .../inrupt/client/caffeine/CaffeineCache.java | 70 +++++++++++++++++++ .../client/caffeine/CaffeineCacheBuilder.java | 56 +++++++++++++++ .../com.inrupt.client.spi.CacheBuilderService | 1 + .../client/caffeine/CaffeineCacheTest.java | 66 +++++++++++++++++ guava/pom.xml | 59 ++++++++++++++++ .../com/inrupt/client/guava/GuavaCache.java | 70 +++++++++++++++++++ .../client/guava/GuavaCacheBuilder.java | 57 +++++++++++++++ .../com.inrupt.client.spi.CacheBuilderService | 1 + .../inrupt/client/guava/GuavaCacheTest.java | 66 +++++++++++++++++ pom.xml | 5 +- reports/pom.xml | 10 +++ 18 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/inrupt/client/ClientCache.java create mode 100644 api/src/main/java/com/inrupt/client/spi/CacheBuilderService.java create mode 100644 api/src/main/java/com/inrupt/client/spi/NoopCache.java create mode 100644 api/src/test/java/com/inrupt/client/spi/NoopCacheTest.java create mode 100644 caffeine/pom.xml create mode 100644 caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCache.java create mode 100644 caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java create mode 100644 caffeine/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService create mode 100644 caffeine/src/test/java/com/inrupt/client/caffeine/CaffeineCacheTest.java create mode 100644 guava/pom.xml create mode 100644 guava/src/main/java/com/inrupt/client/guava/GuavaCache.java create mode 100644 guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java create mode 100644 guava/src/main/resources/META-INF/services/com.inrupt.client.spi.CacheBuilderService create mode 100644 guava/src/test/java/com/inrupt/client/guava/GuavaCacheTest.java 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/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..2976ef264ed --- /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) + .expireAfterAccess(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/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..b9ca881a077 --- /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) + .expireAfterAccess(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/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 From cffb7d2ffc189f001ccd1e15687f155487df67c3 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 11:45:10 -0400 Subject: [PATCH 2/7] OpenId integration --- openid/pom.xml | 6 ++++ .../inrupt/client/openid/OpenIdProvider.java | 32 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) 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.", From 369722dec2c60311fd1dacae7f669060a6b734c8 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 12:42:35 -0400 Subject: [PATCH 3/7] UMA integration --- uma/pom.xml | 6 ++++ .../java/com/inrupt/client/uma/UmaClient.java | 36 +++++++++++++++---- .../com/inrupt/client/uma/UmaClientTest.java | 12 +------ 3 files changed, 37 insertions(+), 17 deletions(-) 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); From 1db2d8e2ee519f1d53321419117350904be6d7fe Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 12:42:54 -0400 Subject: [PATCH 4/7] Examples integration --- examples/cli/pom.xml | 5 +++++ examples/springboot/pom.xml | 10 ++++++++++ examples/webapp/pom.xml | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/examples/cli/pom.xml b/examples/cli/pom.xml index fead82c1dd9..c5b9925d0ac 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 3c085f6e923..5ab556ddb05 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 From 616a4a408832c99028e075339f56087a33fe39fa Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 12:44:01 -0400 Subject: [PATCH 5/7] Archetype integration --- .../java/src/main/resources/archetype-resources/pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From eb8d15fb13861a43d5768bb73772b311ae59ad86 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 7 Apr 2023 13:18:36 -0400 Subject: [PATCH 6/7] Access Grant integration --- access-grant/pom.xml | 6 +++ .../client/accessgrant/AccessGrantClient.java | 36 ++++++++++++--- .../accessgrant/AccessGrantClientTest.java | 45 +++++++------------ 3 files changed, 52 insertions(+), 35 deletions(-) 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); } From 40018a66d7abf42ca87f6b4f92c3f142999c51a4 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Mon, 10 Apr 2023 08:25:45 -0400 Subject: [PATCH 7/7] Adjust cache creation definition --- .../java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java | 2 +- .../main/java/com/inrupt/client/guava/GuavaCacheBuilder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java index 2976ef264ed..a5a5b5cdae3 100644 --- a/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java +++ b/caffeine/src/main/java/com/inrupt/client/caffeine/CaffeineCacheBuilder.java @@ -36,7 +36,7 @@ public class CaffeineCacheBuilder implements CacheBuilderService { public ClientCache build(final int maximumSize, final Duration duration) { return ofCache(Caffeine.newBuilder() .maximumSize(maximumSize) - .expireAfterAccess(duration) + .expireAfterWrite(duration) .build()); } diff --git a/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java b/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java index b9ca881a077..e5b07eb5e1b 100644 --- a/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java +++ b/guava/src/main/java/com/inrupt/client/guava/GuavaCacheBuilder.java @@ -37,7 +37,7 @@ public class GuavaCacheBuilder implements CacheBuilderService { public ClientCache build(final int maximumSize, final Duration duration) { return ofCache(CacheBuilder.newBuilder() .maximumSize(maximumSize) - .expireAfterAccess(duration.getSeconds(), TimeUnit.SECONDS) + .expireAfterWrite(duration.getSeconds(), TimeUnit.SECONDS) .build()); }