diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 60f34f3a1b2..630e99f5280 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -52,6 +52,8 @@ jobs: echo "::error file=DEPENDENCIES,title=Rejected Dependencies found::Some dependencies are marked 'rejected', they cannot be used" exit 1 fi + - name: print expected DEPENDENCIES file + cat DEPENDENCIES-gen - name: Check for differences run: | diff DEPENDENCIES DEPENDENCIES-gen @@ -66,4 +68,4 @@ jobs: run: ./gradlew -Dorg.gradle.jvmargs="-Xmx1g" buildHealth - name: Dependency analysis report - run: cat build/reports/dependency-analysis/build-health-report.txt \ No newline at end of file + run: cat build/reports/dependency-analysis/build-health-report.txt diff --git a/core/common/junit/src/main/java/org/eclipse/edc/junit/testfixtures/TestUtils.java b/core/common/junit/src/main/java/org/eclipse/edc/junit/testfixtures/TestUtils.java index fa25f8f59ff..e1a290cc69c 100644 --- a/core/common/junit/src/main/java/org/eclipse/edc/junit/testfixtures/TestUtils.java +++ b/core/common/junit/src/main/java/org/eclipse/edc/junit/testfixtures/TestUtils.java @@ -20,6 +20,7 @@ import org.eclipse.edc.connector.core.base.EdcHttpClientImpl; import org.eclipse.edc.spi.http.EdcHttpClient; import org.eclipse.edc.spi.monitor.Monitor; +import org.opentest4j.AssertionFailedError; import java.io.File; import java.io.IOException; @@ -33,7 +34,6 @@ import java.util.concurrent.TimeUnit; import static java.lang.String.format; -import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; public class TestUtils { @@ -48,21 +48,29 @@ public class TestUtils { GRADLE_WRAPPER = (System.getProperty("os.name").toLowerCase().contains("win")) ? GRADLE_WRAPPER_WINDOWS : GRADLE_WRAPPER_UNIX; } - public static File getFileFromResourceName(String resourceName) { - URI uri = null; + public static URI getResource(String name) { + var resource = Thread.currentThread().getContextClassLoader().getResource(name); + if (resource == null) { + throw new AssertionFailedError("Cannot find resource " + name); + } try { - uri = Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI(); + return resource.toURI(); } catch (URISyntaxException e) { - fail("Cannot proceed without File : " + resourceName); + throw new AssertionFailedError("Cannot find resource " + name, e); } + } - return new File(uri); + public static File getFileFromResourceName(String resourceName) { + return new File(getResource(resourceName)); } public static String getResourceFileContentAsString(String resourceName) { - var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName); - Scanner s = new Scanner(Objects.requireNonNull(stream, "Not found: " + resourceName)).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { + var scanner = new Scanner(Objects.requireNonNull(stream, "Not found: " + resourceName)).useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; + } catch (IOException e) { + throw new RuntimeException(e); + } } /** diff --git a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/JsonLdExtension.java b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/JsonLdExtension.java index 09207894e9f..c9d6416a11e 100644 --- a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/JsonLdExtension.java +++ b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/JsonLdExtension.java @@ -30,11 +30,10 @@ import org.jetbrains.annotations.NotNull; import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; +import java.net.URI; +import java.net.URISyntaxException; import static java.lang.String.format; -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.eclipse.edc.jsonld.spi.Namespaces.DCAT_PREFIX; import static org.eclipse.edc.jsonld.spi.Namespaces.DCAT_SCHEMA; import static org.eclipse.edc.jsonld.spi.Namespaces.DCT_PREFIX; @@ -93,26 +92,24 @@ public JsonLd createJsonLdService(ServiceExtensionContext context) { service.registerNamespace(ODRL_PREFIX, ODRL_SCHEMA); service.registerNamespace(DSPACE_PREFIX, DSPACE_SCHEMA); - getResourceFile("document" + File.separator + "odrl.jsonld") - .onSuccess(file -> service.registerCachedDocument("http://www.w3.org/ns/odrl.jsonld", file)) + getResourceUri("document" + File.separator + "odrl.jsonld") + .onSuccess(uri -> service.registerCachedDocument("http://www.w3.org/ns/odrl.jsonld", uri)) .onFailure(failure -> monitor.warning("Failed to register cached json-ld document: " + failure.getFailureDetail())); return service; } @NotNull - private Result getResourceFile(String name) { - try (var stream = getClass().getClassLoader().getResourceAsStream(name)) { - if (stream == null) { - return Result.failure(format("Cannot find resource %s", name)); - } - var filename = Path.of(name).getFileName().toString(); - var parts = filename.split("\\."); - var tempFile = Files.createTempFile(parts[0], "." + parts[1]); - Files.copy(stream, tempFile, REPLACE_EXISTING); - return Result.success(tempFile.toFile()); - } catch (Exception e) { - return Result.failure(format("Cannot read resource %s: ", name)); + private Result getResourceUri(String name) { + var uri = getClass().getClassLoader().getResource(name); + if (uri == null) { + return Result.failure(format("Cannot find resource %s", name)); + } + + try { + return Result.success(uri.toURI()); + } catch (URISyntaxException e) { + return Result.failure(format("Cannot read resource %s: %s", name, e.getMessage())); } } diff --git a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/TitaniumJsonLd.java b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/TitaniumJsonLd.java index ad44a9d6b13..3c59affae62 100644 --- a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/TitaniumJsonLd.java +++ b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/TitaniumJsonLd.java @@ -24,11 +24,11 @@ import com.apicatalog.jsonld.loader.HttpLoader; import com.apicatalog.jsonld.loader.SchemeRouter; import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.document.JarLoader; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.result.Result; -import java.io.File; import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -97,8 +97,8 @@ public void registerNamespace(String prefix, String contextIri) { } @Override - public void registerCachedDocument(String contextUrl, File file) { - documentLoader.register(contextUrl, file); + public void registerCachedDocument(String contextUrl, URI uri) { + documentLoader.register(contextUrl, uri); } private JsonObject injectVocab(JsonObject json) { @@ -126,28 +126,30 @@ private JsonObject createContextObject() { private static class CachedDocumentLoader implements DocumentLoader { - private final Map cache = new HashMap<>(); + private final Map cache = new HashMap<>(); private final DocumentLoader loader; CachedDocumentLoader(JsonLdConfiguration configuration) { loader = new SchemeRouter() .set("http", configuration.isHttpEnabled() ? HttpLoader.defaultInstance() : null) .set("https", configuration.isHttpsEnabled() ? HttpLoader.defaultInstance() : null) - .set("file", new FileLoader()); + .set("file", new FileLoader()) + .set("jar", new JarLoader()); } @Override public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { var uri = Optional.of(url.toString()) .map(cache::get) - .map(File::toURI) .orElse(url); return loader.loadDocument(uri, options); } - public void register(String contextUrl, File file) { - cache.put(contextUrl, file); + public void register(String contextUrl, URI uri) { + cache.put(contextUrl, uri); } + } + } diff --git a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/document/JarLoader.java b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/document/JarLoader.java new file mode 100644 index 00000000000..a0a4d436039 --- /dev/null +++ b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/document/JarLoader.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.jsonld.document; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.JsonLdErrorCode; +import com.apicatalog.jsonld.StringUtils; +import com.apicatalog.jsonld.document.Document; +import com.apicatalog.jsonld.document.JsonDocument; +import com.apicatalog.jsonld.document.RdfDocument; +import com.apicatalog.jsonld.http.media.MediaType; +import com.apicatalog.jsonld.loader.DocumentLoader; +import com.apicatalog.jsonld.loader.DocumentLoaderOptions; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.NotNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.NoSuchFileException; +import java.util.Optional; +import java.util.function.Function; + +/** + * Enables loading documents from jar files + */ +public class JarLoader implements DocumentLoader { + + @Override + public Document loadDocument(URI uri, DocumentLoaderOptions options) throws JsonLdError { + if (!"jar".equalsIgnoreCase(uri.getScheme())) { + throw new JsonLdError(JsonLdErrorCode.LOADING_DOCUMENT_FAILED, "Unsupported URL scheme [" + uri.getScheme() + "]. JarLoader accepts only jar scheme."); + } + + try (var is = uri.toURL().openStream()) { + var document = createDocument(uri) + .apply(is) + .orElseThrow(f -> new JsonLdError(JsonLdErrorCode.LOADING_DOCUMENT_FAILED, f.getFailureDetail())); + document.setDocumentUrl(uri); + return document; + + } catch (NoSuchFileException | FileNotFoundException e) { + throw new JsonLdError(JsonLdErrorCode.LOADING_DOCUMENT_FAILED, "File not found [" + uri + "]: " + e.getMessage()); + } catch (IOException e) { + throw new JsonLdError(JsonLdErrorCode.LOADING_DOCUMENT_FAILED, e); + } + } + + @NotNull + private Function> createDocument(URI uri) { + var type = detectedContentType(uri.getSchemeSpecificPart().toLowerCase()) + .orElse(MediaType.JSON); + + if (JsonDocument.accepts(type)) { + return jsonDocumentResolver(type); + } + + if (RdfDocument.accepts(type)) { + return rdfDocumentResolver(type); + } + + return s -> Result.failure("cannot read document"); + } + + @NotNull + private Function> jsonDocumentResolver(MediaType type) { + return stream -> { + try { + return Result.success(JsonDocument.of(type, stream)); + } catch (JsonLdError e) { + return Result.failure(e.getMessage()); + } + }; + } + + @NotNull + private Function> rdfDocumentResolver(MediaType type) { + return stream -> { + try { + return Result.success(RdfDocument.of(type, stream)); + } catch (JsonLdError e) { + return Result.failure(e.getMessage()); + } + }; + } + + private Optional detectedContentType(String name) { + if (name == null || StringUtils.isBlank(name)) { + return Optional.empty(); + } + if (name.endsWith(".nq")) { + return Optional.of(MediaType.N_QUADS); + } + if (name.endsWith(".json")) { + return Optional.of(MediaType.JSON); + } + if (name.endsWith(".jsonld")) { + return Optional.of(MediaType.JSON_LD); + } + if (name.endsWith(".html")) { + return Optional.of(MediaType.HTML); + } + + return Optional.empty(); + } +} diff --git a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/TitaniumJsonLdTest.java b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/TitaniumJsonLdTest.java index 331dbe1c1ed..a128ae9ef12 100644 --- a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/TitaniumJsonLdTest.java +++ b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/TitaniumJsonLdTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; -import java.io.File; +import java.net.URI; import static jakarta.json.Json.createArrayBuilder; import static jakarta.json.Json.createObjectBuilder; @@ -43,8 +43,7 @@ class TitaniumJsonLdTest { private final int port = getFreePort(); private final ClientAndServer server = startClientAndServer(port); - - private final Monitor monitor = mock(Monitor.class); + private final Monitor monitor = mock(); @AfterEach void tearDown() { @@ -150,7 +149,7 @@ void documentResolution_shouldNotCallHttpEndpoint_whenFileContextIsRegistered() .add("test:key", "value") .build(); var service = defaultService(); - service.registerCachedDocument(contextUrl, getFileFromResourceName("test-context.jsonld")); + service.registerCachedDocument(contextUrl, getFileFromResourceName("test-context.jsonld").toURI()); var expanded = service.expand(jsonObject); @@ -172,7 +171,7 @@ void documentResolution_shouldFailByDefault_whenContextIsNotRegisteredAndHttpIsN .add("test:key", "value") .build(); var service = defaultService(); - service.registerCachedDocument("http//any.other/url", new File("any")); + service.registerCachedDocument("http//any.other/url", URI.create("any:uri")); var expanded = service.expand(jsonObject); @@ -188,7 +187,7 @@ void documentResolution_shouldCallHttpEndpoint_whenContextIsNotRegistered_andHtt .add("test:key", "value") .build(); var service = httpEnabledService(); - service.registerCachedDocument("http//any.other/url", new File("any")); + service.registerCachedDocument("http//any.other/url", URI.create("any:uri")); var expanded = service.expand(jsonObject); diff --git a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/document/JarLoaderTest.java b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/document/JarLoaderTest.java new file mode 100644 index 00000000000..818d9cc5b85 --- /dev/null +++ b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/document/JarLoaderTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.jsonld.document; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.loader.DocumentLoaderOptions; +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getResource; + +class JarLoaderTest { + + private final JarLoader jarLoader = new JarLoader(); + + @Test + void shouldLoadJsonSchemaFromJar() throws JsonLdError { + var jarUri = getResource("jar-with-resource-example.jar"); + var resourceInJarUri = URI.create(format("jar:%s!/document/odrl.jsonld", jarUri)); + + var document = jarLoader.loadDocument(resourceInJarUri, new DocumentLoaderOptions()); + + assertThat(document).isNotNull(); + } + + @Test + void shouldThrowError_whenSchemeIsNotJar() { + assertThatThrownBy(() -> jarLoader.loadDocument(URI.create("file://tmp/any"), new DocumentLoaderOptions())) + .isInstanceOf(JsonLdError.class) + .hasMessageStartingWith("Unsupported URL scheme"); + } + + @Test + void shouldThrowErrorWhenJarFileNotFound() { + var resourceInJarUri = URI.create("jar:file:/tmp/unexistent-file.jar!/document/odrl.jsonld"); + + assertThatThrownBy(() -> jarLoader.loadDocument(resourceInJarUri, new DocumentLoaderOptions())) + .isInstanceOf(JsonLdError.class) + .hasMessageStartingWith("File not found"); + } + + @Test + void shouldThrowErrorWhenMalformedUrl() { + var resourceInJarUri = URI.create("jar:/malformed/url!/document/odrl.jsonld"); + + assertThatThrownBy(() -> jarLoader.loadDocument(resourceInJarUri, new DocumentLoaderOptions())) + .isInstanceOf(JsonLdError.class); + } + + @Test + void shouldThrowErrorWhenDocumentFileNotFoundInTheJar() { + var jarUri = getResource("jar-with-resource-example.jar"); + var resourceInJarUri = URI.create(format("jar:%s!/document/not-existent.jsonld", jarUri)); + + assertThatThrownBy(() -> jarLoader.loadDocument(resourceInJarUri, new DocumentLoaderOptions())) + .isInstanceOf(JsonLdError.class) + .hasMessageStartingWith("File not found"); + } + +} diff --git a/extensions/common/json-ld/src/test/resources/.gitignore b/extensions/common/json-ld/src/test/resources/.gitignore new file mode 100644 index 00000000000..ddb07bbf963 --- /dev/null +++ b/extensions/common/json-ld/src/test/resources/.gitignore @@ -0,0 +1,2 @@ +# enable jars for this folder +!*.jar diff --git a/extensions/common/json-ld/src/test/resources/jar-with-resource-example.jar b/extensions/common/json-ld/src/test/resources/jar-with-resource-example.jar new file mode 100644 index 00000000000..55e2a1a7aeb Binary files /dev/null and b/extensions/common/json-ld/src/test/resources/jar-with-resource-example.jar differ diff --git a/spi/common/json-ld-spi/src/main/java/org/eclipse/edc/jsonld/spi/JsonLd.java b/spi/common/json-ld-spi/src/main/java/org/eclipse/edc/jsonld/spi/JsonLd.java index 51154dc4b61..32e5a317c3a 100644 --- a/spi/common/json-ld-spi/src/main/java/org/eclipse/edc/jsonld/spi/JsonLd.java +++ b/spi/common/json-ld-spi/src/main/java/org/eclipse/edc/jsonld/spi/JsonLd.java @@ -18,6 +18,7 @@ import org.eclipse.edc.spi.result.Result; import java.io.File; +import java.net.URI; /** * Provides JsonLD expansion/compaction functionalities. @@ -48,6 +49,16 @@ public interface JsonLd { */ void registerNamespace(String prefix, String contextIri); + /** + * Register a JsonLD file document loader. + * When a document is cached, that url won't be called through http/https, but the content will be + * loaded from the URI parameter. + * + * @param url the url + * @param uri the file + */ + void registerCachedDocument(String url, URI uri); + /** * Register a JsonLD file document loader. * When an url is registered with a File, that url won't be called through http/https, but the content will be @@ -55,7 +66,11 @@ public interface JsonLd { * * @param url the url * @param file the file + * @deprecated please use {@link #registerCachedDocument(String, URI)} */ - void registerCachedDocument(String url, File file); + @Deprecated(since = "0.1.3") + default void registerCachedDocument(String url, File file) { + registerCachedDocument(url, file.toURI()); + } }