diff --git a/README.md b/README.md index 7553c32e..ca93ed41 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,19 @@ static DocumentLoader LOADER = HttpLoader.defaultInstance().timeount(Duration.of JsonLd.expand(...).loader(LOADER).get(); ``` +#### Document caching +Configure LRU-based cache for loading documents. +The argument determines size of the LRU-cache. +```javascript +JsonLd.toRdf("https://example/document.jsonld").loader(new LRUDocumentCache(loader, 12)).get(); +``` + +You can share instance of `LRUDocumentCache` among multiple calls to reuse cached documents. +```javascript +DocumentLoader cachedLoader = new LRUDocumentCache(12); +JsonLd.toRdf("https://example/document.jsonld").loader(cachedLoader).get(); +JsonLd.toRdf("https://example/another-document.jsonld").loader(cachedLoader).get(); +``` ## Contributing diff --git a/src/main/java/com/apicatalog/jsonld/loader/DocumentLoaderOptions.java b/src/main/java/com/apicatalog/jsonld/loader/DocumentLoaderOptions.java index 220ced08..db05eb57 100644 --- a/src/main/java/com/apicatalog/jsonld/loader/DocumentLoaderOptions.java +++ b/src/main/java/com/apicatalog/jsonld/loader/DocumentLoaderOptions.java @@ -16,6 +16,8 @@ package com.apicatalog.jsonld.loader; import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; /** * The {@link DocumentLoaderOptions} is used to pass various options to the @@ -64,4 +66,49 @@ public void setRequestProfile(Collection requestProfile) { this.requestProfile = requestProfile; } + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + DocumentLoaderOptions options = (DocumentLoaderOptions) other; + if (extractAllScripts != options.extractAllScripts || + !Objects.equals(profile, options.profile)) { + return false; + } + // We need to deal with the collection of profiles. + // We assume that the order does matter. + if (requestProfile == null && options.requestProfile == null) { + // They are bot null. + return true; + } + if (requestProfile == null || options.requestProfile == null) { + // Only one is null. + return false; + } + if (requestProfile.size() != options.requestProfile.size()) { + // Different size. + return false; + } + // We need to be sure the content is the same. + Iterator thisIterator = requestProfile.iterator(); + Iterator otherIterator = options.requestProfile.iterator(); + while (thisIterator.hasNext() && otherIterator.hasNext()) { + if (!Objects.equals(thisIterator.next(), otherIterator.next())) { + // One value is not the same. + return false; + } + } + // We have not found a difference thus they are the same. + return true; + } + + @Override + public int hashCode() { + return Objects.hash(extractAllScripts, profile, requestProfile); + } + } diff --git a/src/main/java/com/apicatalog/jsonld/loader/LRUDocumentCache.java b/src/main/java/com/apicatalog/jsonld/loader/LRUDocumentCache.java new file mode 100644 index 00000000..5d2579db --- /dev/null +++ b/src/main/java/com/apicatalog/jsonld/loader/LRUDocumentCache.java @@ -0,0 +1,66 @@ +package com.apicatalog.jsonld.loader; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.context.cache.LruCache; +import com.apicatalog.jsonld.document.Document; + +import java.net.URI; +import java.util.Objects; + +public class LRUDocumentCache implements DocumentLoader { + + private final DocumentLoader documentLoader; + + private final LruCache cache; + + protected static class CacheKey { + + private final URI url; + + private final DocumentLoaderOptions options; + + public CacheKey(URI url, DocumentLoaderOptions options) { + this.url = url; + this.options = options; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + CacheKey cacheKey = (CacheKey) other; + return Objects.equals(url, cacheKey.url) && + Objects.equals(options, cacheKey.options); + } + + @Override + public int hashCode() { + return Objects.hash(url, options); + } + } + + public LRUDocumentCache(DocumentLoader documentLoader, int cacheSize) { + this.documentLoader = documentLoader; + this.cache = new LruCache<>(cacheSize); + } + + @Override + public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { + Object key = createCacheKey(url, options); + Document result = cache.get(key); + if (result == null) { + result = documentLoader.loadDocument(url, options); + cache.put(key, result); + } + return result; + } + + protected Object createCacheKey(URI url, DocumentLoaderOptions options){ + return new CacheKey(url, options); + } + +} diff --git a/src/test/java/com/apicatalog/jsonld/loader/LRUDocumentCacheTest.java b/src/test/java/com/apicatalog/jsonld/loader/LRUDocumentCacheTest.java new file mode 100644 index 00000000..d2d73c16 --- /dev/null +++ b/src/test/java/com/apicatalog/jsonld/loader/LRUDocumentCacheTest.java @@ -0,0 +1,172 @@ +package com.apicatalog.jsonld.loader; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.document.Document; +import com.apicatalog.jsonld.document.JsonDocument; +import jakarta.json.JsonValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class LRUDocumentCacheTest { + + static class Request { + + final URI url; + + final DocumentLoaderOptions options; + + public Request(URI url, DocumentLoaderOptions options) { + this.url = url; + this.options = options; + } + + } + + static class RecordRequestLoader implements DocumentLoader { + + final List requests = new ArrayList<>(); + + @Override + public Document loadDocument(URI url, DocumentLoaderOptions options) { + requests.add(new Request(url, options)); + // Return empty document. + return JsonDocument.of(JsonValue.EMPTY_JSON_ARRAY); + } + + } + + @Test + void testLoadDocument() throws JsonLdError { + RecordRequestLoader loader = new RecordRequestLoader(); + LRUDocumentCache cachedLoader = new LRUDocumentCache(loader, 2); + + DocumentLoaderOptions options = new DocumentLoaderOptions(); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + // There should be only one call as all other should be cached. + Assertions.assertEquals(1, loader.requests.size()); + + // Make sure valid arguments were passed. + Request request = loader.requests.get(0); + Assertions.assertEquals("http://localhost/1",request.url.toString()); + Assertions.assertSame(options,request.options); + } + + @Test + void testCacheSize() throws JsonLdError { + RecordRequestLoader loader = new RecordRequestLoader(); + LRUDocumentCache cachedLoader = new LRUDocumentCache(loader, 2); + + DocumentLoaderOptions options = new DocumentLoaderOptions(); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + cachedLoader.loadDocument(URI.create("http://localhost/2"), options); + cachedLoader.loadDocument(URI.create("http://localhost/2"), options); + + // There should be only one call as all other should be cached. + Assertions.assertEquals(2, loader.requests.size()); + + // Request of new resource. + cachedLoader.loadDocument(URI.create("http://localhost/3"), options); + Assertions.assertEquals(3, loader.requests.size()); + + // Using LRU the first resources should not be in cache anymore. + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + Assertions.assertEquals(4, loader.requests.size()); + } + + @Test + void testLoadDocumentsWithDifferentOptions() throws JsonLdError { + RecordRequestLoader loader = new RecordRequestLoader(); + LRUDocumentCache cachedLoader = new LRUDocumentCache(loader, 2); + + // Using options with same inside should lead to cache hit. + DocumentLoaderOptions options = new DocumentLoaderOptions(); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + DocumentLoaderOptions sameOptions = new DocumentLoaderOptions(); + cachedLoader.loadDocument(URI.create("http://localhost/1"), sameOptions); + Assertions.assertEquals(1, loader.requests.size()); + + // Use of different options should cause cache miss. + DocumentLoaderOptions differentOptions = new DocumentLoaderOptions(); + differentOptions.setProfile("profile"); + cachedLoader.loadDocument(URI.create("http://localhost/1"), differentOptions); + Assertions.assertEquals(2, loader.requests.size()); + } + + @Test + void testCachingEqualOptions() throws JsonLdError { + RecordRequestLoader loader = new RecordRequestLoader(); + LRUDocumentCache cachedLoader = new LRUDocumentCache(loader, 2); + DocumentLoaderOptions options = null; + + options = new DocumentLoaderOptions(); + options.setProfile("profile"); + options.setExtractAllScripts(true); + List firstList = new ArrayList<>(); + firstList.add("first"); + firstList.add("second"); + options.setRequestProfile(firstList); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + options = new DocumentLoaderOptions(); + options.setProfile("profile"); + options.setExtractAllScripts(true); + List secondList = new ArrayList<>(); + secondList.add("first"); + secondList.add("second"); + options.setRequestProfile(secondList); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + options = new DocumentLoaderOptions(); + options.setProfile("profile"); + options.setExtractAllScripts(true); + List thirdList = new LinkedList<>(); + thirdList.add("first"); + thirdList.add("second"); + options.setRequestProfile(thirdList); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + Assertions.assertEquals(1, loader.requests.size()); + } + + @Test + void testCachingProfilesOrderMatter() throws JsonLdError { + RecordRequestLoader loader = new RecordRequestLoader(); + LRUDocumentCache cachedLoader = new LRUDocumentCache(loader, 2); + DocumentLoaderOptions options = null; + + options = new DocumentLoaderOptions(); + options.setProfile("profile"); + options.setExtractAllScripts(true); + List firstList = new ArrayList<>(); + firstList.add("first"); + firstList.add("second"); + options.setRequestProfile(firstList); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + options = new DocumentLoaderOptions(); + options.setProfile("profile"); + options.setExtractAllScripts(true); + List secondList = new ArrayList<>(); + secondList.add("second"); + secondList.add("first"); + options.setRequestProfile(secondList); + cachedLoader.loadDocument(URI.create("http://localhost/1"), options); + + Assertions.assertEquals(2, loader.requests.size()); + } + + +}