From 359838421573affb5f3c0386dcab26afebca3887 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 17:22:20 +0000 Subject: [PATCH 01/47] chore(deps): update dependency gradle to v8.14.1 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c83..002b867c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From a5896f53c22151eade0e1d67d5a0dd966ef5a664 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 23:13:00 +0000 Subject: [PATCH 02/47] chore(deps): update dependency com.networknt:json-schema-validator to v1.5.7 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f7fcd613..e0258097 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ dependencies { // JSON-LD, Zenodo mapping implementation group: 'com.apicatalog', name: 'titanium-json-ld', version: '1.6.0' // metadata validation, profiles based on JSON schema - implementation group: "com.networknt", name: "json-schema-validator", version: "1.5.6" + implementation group: "com.networknt", name: "json-schema-validator", version: "1.5.7" implementation 'org.glassfish:jakarta.json:2.0.1' //JTE for template processing implementation('gg.jte:jte:3.2.1') From 355b8560d96afe33978338c7c8725595016b1c55 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 28 May 2025 14:32:17 +0200 Subject: [PATCH 03/47] refactor: rename ContextTest to RoCrateMetadataContextTest for clarity --- .../{ContextTest.java => RoCrateMetadataContextTest.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/test/java/edu/kit/datamanager/ro_crate/context/{ContextTest.java => RoCrateMetadataContextTest.java} (98%) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java similarity index 98% rename from src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java rename to src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index 4995f272..21375464 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class ContextTest { +public class RoCrateMetadataContextTest { RoCrateMetadataContext context; RoCrateMetadataContext complexContext; @@ -32,7 +32,7 @@ void initContext() throws IOException { final String crateManifestPath = "/crates/extendedContextExample/ro-crate-metadata.json"; ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode jsonNode = objectMapper.readTree(ContextTest.class.getResourceAsStream(crateManifestPath)); + JsonNode jsonNode = objectMapper.readTree(RoCrateMetadataContextTest.class.getResourceAsStream(crateManifestPath)); this.complexContext = new RoCrateMetadataContext(jsonNode.get("@context")); } From 9d639bd96b72115569424799e51e84d5cf407531 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 28 May 2025 14:46:44 +0200 Subject: [PATCH 04/47] test: add Context tests for new context implementation --- .../ro_crate/context/ContextTest.java | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java new file mode 100644 index 00000000..94d5cad3 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java @@ -0,0 +1,254 @@ +package edu.kit.datamanager.ro_crate.context; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +class ContextTest { + private Context context; + private static final String SCHEMA_URL = "https://schema.org/"; + private static final String EXAMPLE_URL = "http://example.org/terms/"; + + @BeforeEach + void setUp() { + context = new Context(); + } + + @Nested + class CreationAndBasics { + @Test + void defaultConstructor_shouldCreateEmptyContext() { + ObjectNode json = context.toJsonLd(); + assertNotNull(json.get("@context")); + assertTrue(json.get("@context").isEmpty()); + } + + @Test + void fromJson_withValidContext_shouldCreateEquivalentContext() { + ObjectMapper mapper = MyObjectMapper.getMapper(); + ObjectNode input = mapper.createObjectNode(); + input.put("schema", SCHEMA_URL); + + Context newContext = Context.fromJson(input); + assertEquals(SCHEMA_URL, newContext.getDefinition("schema")); + } + + @Test + void fromJson_withInvalidJson_shouldThrowException() { + assertThrows(IllegalArgumentException.class, () -> + Context.fromJson(null) + ); + } + } + + @Nested + class TermDefinitions { + @Test + void defineTerm_withValidTuple_shouldAddDefinition() { + context.define("name", "https://schema.org/name"); + assertTrue(context.isValidTerm("name")); + assertEquals("https://schema.org/name", context.getDefinition("name")); + } + + @Test + void defineTerm_withNull_shouldThrowException() { + assertThrows(IllegalArgumentException.class, () -> + context.define(null, "https://example.org") + ); + assertThrows(IllegalArgumentException.class, () -> + context.define("term", null) + ); + } + + @Test + void defineTerm_withInvalidIRI_shouldThrowException() { + assertThrows(IllegalArgumentException.class, () -> + context.define("term", "not a valid IRI") + ); + } + } + + @Nested + class PrefixHandling { + @Test + void definePrefix_withValidIRI_shouldAllowPrefixedTerms() { + context.define("schema", SCHEMA_URL); + assertTrue(context.isValidTerm("schema:name")); + } + + @Test + void validateTerm_withUndefinedPrefix_shouldReturnFalse() { + assertFalse(context.isValidTerm("undefined:term")); + } + + @Test + void validateTerm_withDefinedPrefixButInvalidTerm_shouldReturnFalse() { + context.define("ex", EXAMPLE_URL); + assertFalse(context.isValidTerm("ex:nonexistentTerm")); + } + } + + @Nested + class ContextResolution { + @Test + void addContext_withResolvableUrl_shouldExpandContext() { + context.addContext(URI.create(SCHEMA_URL)); + assertTrue(context.isValidTerm("Person")); + assertTrue(context.isValidTerm("name")); + } + + @Test + void addContext_withUnresolvableUrl_shouldThrow() { + URI unreachableUri = URI.create("http://nonexistent.example.org/"); + assertThrows(ContextLoadException.class, () -> + context.addContext(unreachableUri) + ); + } + + @Test + void addContext_withLocalFile_shouldLoadFromFileSystem(@TempDir Path tempDir) throws IOException { + Path contextFile = tempDir.resolve("test-context.jsonld"); + Files.writeString(contextFile, """ + { + "@context": { + "test": "http://example.org/test#", + "LocalTerm": "test:LocalTerm" + } + } + """); + + context.addContext(contextFile.toUri()); + assertTrue(context.isValidTerm("LocalTerm")); + } + + @Test + void addContext_withNonexistentLocalFile_shouldThrow(@TempDir Path tempDir) { + Path nonExistentFile = tempDir.resolve("nonexistent.jsonld"); + URI fileUri = nonExistentFile.toUri(); + + assertThrows(ContextLoadException.class, () -> + context.addContext(fileUri) + ); + } + + @Test + void addContext_withInvalidContent_shouldThrow(@TempDir Path tempDir) throws IOException { + Path invalidFile = tempDir.resolve("invalid.jsonld"); + Files.writeString(invalidFile, "{ invalid json"); + + assertThrows(ContextLoadException.class, () -> + context.addContext(invalidFile.toUri()) + ); + } + } + + @Nested + class TermValidation { + @Test + void isValidTerm_withAbsoluteIRI_shouldReturnTrue() { + assertTrue(context.isValidTerm("https://schema.org/Person")); + } + + @Test + void isValidTerm_withRelativeIRI_shouldReturnFalse() { + assertFalse(context.isValidTerm("Person")); + } + + @Test + void isValidTerm_withDefinedTerm_shouldReturnTrue() { + context.define("person", "https://schema.org/Person"); + assertTrue(context.isValidTerm("person")); + } + + @Test + void isValidTerm_withPrefixedTerm_shouldValidateAgainstPrefix() { + context.define("schema", SCHEMA_URL); + assertTrue(context.isValidTerm("schema:Person")); + assertFalse(context.isValidTerm("schema:NonexistentType")); + } + } + + @Nested + class JsonSerialization { + @Test + void toJsonLd_withEmptyContext_shouldReturnMinimalStructure() { + ObjectNode json = context.toJsonLd(); + assertNotNull(json.get("@context")); + assertTrue(json.get("@context").isEmpty()); + } + + @Test + void toJsonLd_withDefinitions_shouldIncludeAllDefinitions() { + context.define("schema", SCHEMA_URL); + context.define("name", "https://schema.org/name"); + + ObjectNode json = context.toJsonLd(); + JsonNode contextNode = json.get("@context"); + assertEquals(SCHEMA_URL, contextNode.get("schema").asText()); + assertEquals("https://schema.org/name", contextNode.get("name").asText()); + } + } + + @Nested + class ContextModification { + @Test + void remove_existingDefinition_shouldRemoveDefinition() { + context.define("term", "https://example.org/term"); + assertTrue(context.isValidTerm("term")); + + context.remove("term"); + assertFalse(context.isValidTerm("term")); + } + + @Test + void remove_nonexistentDefinition_shouldNotThrowException() { + assertDoesNotThrow(() -> context.remove("nonexistent")); + } + + @Test + void getDefinitions_shouldReturnUnmodifiableMap() { + context.define("term", "https://example.org/term"); + Map definitions = context.getDefinitions(); + + assertTrue(definitions.containsKey("term")); + assertTrue(definitions.containsValue("https://example.org/term")); + + assertThrows(UnsupportedOperationException.class, () -> + definitions.put("new", "value") + ); + } + + @Test + void removeContext_shouldRemoveAllAssociatedTerms() throws IOException { + URI contextUri = URI.create(SCHEMA_URL); + context.addContext(contextUri); + assertTrue(context.isValidTerm("Person")); + + context.removeContext(contextUri); + assertFalse(context.isValidTerm("Person")); + } + + @Test + void getDefinitions_shouldIncludeContextTerms() throws IOException { + URI contextUri = URI.create(SCHEMA_URL); + context.addContext(contextUri); + Map definitions = context.getDefinitions(); + + assertTrue(definitions.containsKey("Person")); + assertTrue(definitions.containsValue(SCHEMA_URL + "Person")); + } + } +} From 8803c60b8ce8cd480a34cd67b29f1b0e93426783 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 2 Jun 2025 15:55:50 +0200 Subject: [PATCH 05/47] feat: add support for context prefixes in entity checks --- .../context/RoCrateMetadataContext.java | 10 +- .../ro_crate/context/ContextTest.java | 254 ------------------ .../context/RoCrateMetadataContextTest.java | 18 ++ 3 files changed, 26 insertions(+), 256 deletions(-) delete mode 100644 src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java index cce6a7e9..c4f5a8f2 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; import edu.kit.datamanager.ro_crate.special.IdentifierUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -117,6 +118,11 @@ public boolean checkEntity(AbstractEntity entity) { entity.getProperties().path("@type"), new TypeReference<>() {} ); + + final Function isFail = checkMeStr -> this.contextMap.get(checkMeStr) == null + && this.contextMap.keySet().stream() + .noneMatch(key -> checkMeStr.startsWith(key + ":")); + // check if the items in the array of types are present in the context for (String s : types) { // special cases: @@ -134,7 +140,7 @@ public boolean checkEntity(AbstractEntity entity) { continue; } - if (this.contextMap.get(s) == null) { + if (isFail.apply(s)) { System.err.println("type " + s + " is missing from the context!"); return false; } @@ -147,7 +153,7 @@ public boolean checkEntity(AbstractEntity entity) { // full URLs are considered fine continue; } - if (this.contextMap.get(s) == null) { + if (isFail.apply(s)) { System.err.println("attribute name " + s + " is missing from context;"); return false; } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java deleted file mode 100644 index 94d5cad3..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java +++ /dev/null @@ -1,254 +0,0 @@ -package edu.kit.datamanager.ro_crate.context; - -import static org.junit.jupiter.api.Assertions.*; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.io.TempDir; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -class ContextTest { - private Context context; - private static final String SCHEMA_URL = "https://schema.org/"; - private static final String EXAMPLE_URL = "http://example.org/terms/"; - - @BeforeEach - void setUp() { - context = new Context(); - } - - @Nested - class CreationAndBasics { - @Test - void defaultConstructor_shouldCreateEmptyContext() { - ObjectNode json = context.toJsonLd(); - assertNotNull(json.get("@context")); - assertTrue(json.get("@context").isEmpty()); - } - - @Test - void fromJson_withValidContext_shouldCreateEquivalentContext() { - ObjectMapper mapper = MyObjectMapper.getMapper(); - ObjectNode input = mapper.createObjectNode(); - input.put("schema", SCHEMA_URL); - - Context newContext = Context.fromJson(input); - assertEquals(SCHEMA_URL, newContext.getDefinition("schema")); - } - - @Test - void fromJson_withInvalidJson_shouldThrowException() { - assertThrows(IllegalArgumentException.class, () -> - Context.fromJson(null) - ); - } - } - - @Nested - class TermDefinitions { - @Test - void defineTerm_withValidTuple_shouldAddDefinition() { - context.define("name", "https://schema.org/name"); - assertTrue(context.isValidTerm("name")); - assertEquals("https://schema.org/name", context.getDefinition("name")); - } - - @Test - void defineTerm_withNull_shouldThrowException() { - assertThrows(IllegalArgumentException.class, () -> - context.define(null, "https://example.org") - ); - assertThrows(IllegalArgumentException.class, () -> - context.define("term", null) - ); - } - - @Test - void defineTerm_withInvalidIRI_shouldThrowException() { - assertThrows(IllegalArgumentException.class, () -> - context.define("term", "not a valid IRI") - ); - } - } - - @Nested - class PrefixHandling { - @Test - void definePrefix_withValidIRI_shouldAllowPrefixedTerms() { - context.define("schema", SCHEMA_URL); - assertTrue(context.isValidTerm("schema:name")); - } - - @Test - void validateTerm_withUndefinedPrefix_shouldReturnFalse() { - assertFalse(context.isValidTerm("undefined:term")); - } - - @Test - void validateTerm_withDefinedPrefixButInvalidTerm_shouldReturnFalse() { - context.define("ex", EXAMPLE_URL); - assertFalse(context.isValidTerm("ex:nonexistentTerm")); - } - } - - @Nested - class ContextResolution { - @Test - void addContext_withResolvableUrl_shouldExpandContext() { - context.addContext(URI.create(SCHEMA_URL)); - assertTrue(context.isValidTerm("Person")); - assertTrue(context.isValidTerm("name")); - } - - @Test - void addContext_withUnresolvableUrl_shouldThrow() { - URI unreachableUri = URI.create("http://nonexistent.example.org/"); - assertThrows(ContextLoadException.class, () -> - context.addContext(unreachableUri) - ); - } - - @Test - void addContext_withLocalFile_shouldLoadFromFileSystem(@TempDir Path tempDir) throws IOException { - Path contextFile = tempDir.resolve("test-context.jsonld"); - Files.writeString(contextFile, """ - { - "@context": { - "test": "http://example.org/test#", - "LocalTerm": "test:LocalTerm" - } - } - """); - - context.addContext(contextFile.toUri()); - assertTrue(context.isValidTerm("LocalTerm")); - } - - @Test - void addContext_withNonexistentLocalFile_shouldThrow(@TempDir Path tempDir) { - Path nonExistentFile = tempDir.resolve("nonexistent.jsonld"); - URI fileUri = nonExistentFile.toUri(); - - assertThrows(ContextLoadException.class, () -> - context.addContext(fileUri) - ); - } - - @Test - void addContext_withInvalidContent_shouldThrow(@TempDir Path tempDir) throws IOException { - Path invalidFile = tempDir.resolve("invalid.jsonld"); - Files.writeString(invalidFile, "{ invalid json"); - - assertThrows(ContextLoadException.class, () -> - context.addContext(invalidFile.toUri()) - ); - } - } - - @Nested - class TermValidation { - @Test - void isValidTerm_withAbsoluteIRI_shouldReturnTrue() { - assertTrue(context.isValidTerm("https://schema.org/Person")); - } - - @Test - void isValidTerm_withRelativeIRI_shouldReturnFalse() { - assertFalse(context.isValidTerm("Person")); - } - - @Test - void isValidTerm_withDefinedTerm_shouldReturnTrue() { - context.define("person", "https://schema.org/Person"); - assertTrue(context.isValidTerm("person")); - } - - @Test - void isValidTerm_withPrefixedTerm_shouldValidateAgainstPrefix() { - context.define("schema", SCHEMA_URL); - assertTrue(context.isValidTerm("schema:Person")); - assertFalse(context.isValidTerm("schema:NonexistentType")); - } - } - - @Nested - class JsonSerialization { - @Test - void toJsonLd_withEmptyContext_shouldReturnMinimalStructure() { - ObjectNode json = context.toJsonLd(); - assertNotNull(json.get("@context")); - assertTrue(json.get("@context").isEmpty()); - } - - @Test - void toJsonLd_withDefinitions_shouldIncludeAllDefinitions() { - context.define("schema", SCHEMA_URL); - context.define("name", "https://schema.org/name"); - - ObjectNode json = context.toJsonLd(); - JsonNode contextNode = json.get("@context"); - assertEquals(SCHEMA_URL, contextNode.get("schema").asText()); - assertEquals("https://schema.org/name", contextNode.get("name").asText()); - } - } - - @Nested - class ContextModification { - @Test - void remove_existingDefinition_shouldRemoveDefinition() { - context.define("term", "https://example.org/term"); - assertTrue(context.isValidTerm("term")); - - context.remove("term"); - assertFalse(context.isValidTerm("term")); - } - - @Test - void remove_nonexistentDefinition_shouldNotThrowException() { - assertDoesNotThrow(() -> context.remove("nonexistent")); - } - - @Test - void getDefinitions_shouldReturnUnmodifiableMap() { - context.define("term", "https://example.org/term"); - Map definitions = context.getDefinitions(); - - assertTrue(definitions.containsKey("term")); - assertTrue(definitions.containsValue("https://example.org/term")); - - assertThrows(UnsupportedOperationException.class, () -> - definitions.put("new", "value") - ); - } - - @Test - void removeContext_shouldRemoveAllAssociatedTerms() throws IOException { - URI contextUri = URI.create(SCHEMA_URL); - context.addContext(contextUri); - assertTrue(context.isValidTerm("Person")); - - context.removeContext(contextUri); - assertFalse(context.isValidTerm("Person")); - } - - @Test - void getDefinitions_shouldIncludeContextTerms() throws IOException { - URI contextUri = URI.create(SCHEMA_URL); - context.addContext(contextUri); - Map definitions = context.getDefinitions(); - - assertTrue(definitions.containsKey("Person")); - assertTrue(definitions.containsValue(SCHEMA_URL + "Person")); - } - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index 21375464..d546fb7e 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; import edu.kit.datamanager.ro_crate.entities.data.DataEntity; @@ -278,4 +279,21 @@ void testReadPairs() { // prove immutability assertThrows(UnsupportedOperationException.class, () -> given.put("newKey", "newValue")); } + + @Test + void checkEntity_withDefinedPrefixedType_succeeds() throws IOException { + // assume we read a crate just for demonstration + RoCrate crate = new RoCrate(); + // and we extend the context + RoCrateMetadataContext context = new RoCrateMetadataContext(); + context.addToContext("rdfs", "https://www.w3.org/2000/01/rdf-schema#"); + crate.setMetadataContext(context); + // then we use the new context + DataEntity entity = new DataEntity.DataEntityBuilder() + .addType("rdfs:Property") + .build(); + crate.addDataEntity(entity); + // Then we expect this to work + assertTrue(context.checkEntity(entity)); + } } From 4afd85ecf795b41df5435b2556acb951be887f20 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 2 Jun 2025 15:56:03 +0200 Subject: [PATCH 06/47] feat: add entity checks in RoCrateBuilder for data and contextual entities --- src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java index 356b0159..32c22dec 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -375,12 +375,14 @@ public RoCrateBuilder addDescription(String description) { * @return returns the builder for further usage. */ public RoCrateBuilder addDataEntity(DataEntity dataEntity) { + this.metadataContext.checkEntity(dataEntity); this.payload.addDataEntity(dataEntity); this.rootDataEntity.addToHasPart(dataEntity.getId()); return this; } public RoCrateBuilder addContextualEntity(ContextualEntity contextualEntity) { + this.metadataContext.checkEntity(contextualEntity); this.payload.addContextualEntity(contextualEntity); return this; } From 78b1957c561e5724077bf1fbd96d8735dfb995e7 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 2 Jun 2025 15:57:48 +0200 Subject: [PATCH 07/47] chore: fix typo in test and add entity check --- .../ro_crate/context/RoCrateMetadataContextTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index d546fb7e..1fb7b574 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -99,8 +99,8 @@ void creationFromPairsJsonTest() { var objectMapper = MyObjectMapper.getMapper(); ObjectNode rawContext = objectMapper.createObjectNode(); - rawContext.put("house", "www.example.con/house"); - rawContext.put("road", "www.example.con/road"); + rawContext.put("house", "www.example.com/house"); + rawContext.put("road", "www.example.com/road"); ObjectNode rawCrate = objectMapper.createObjectNode(); rawCrate.set("@context", rawContext); @@ -108,6 +108,13 @@ void creationFromPairsJsonTest() { assertNotNull(newContext); HelpFunctions.compare(newContext.getContextJsonEntity(), rawCrate, true); + + var entityWithTerms = new ContextualEntity.ContextualEntityBuilder() + .setId("dkfaj") + .addType("house") + .addType("road") + .build(); + assertTrue(newContext.checkEntity(entityWithTerms)); } @Test From f809820115ca1adb1e894ca93c091d112f4e610f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 16:19:43 +0000 Subject: [PATCH 08/47] chore(deps): update dependency org.junit:junit-bom to v5.13.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e0258097..8d162355 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { dependencies { // JUnit setup for testing - testImplementation(platform("org.junit:junit-bom:5.12.2")) + testImplementation(platform("org.junit:junit-bom:5.13.0")) testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly('org.junit.platform:junit-platform-launcher') // JSON object mapping / (de-)serialization From 39518cc0a0207aec77d32a17e4b13356c8c24617 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 2 Jun 2025 17:09:14 +0200 Subject: [PATCH 09/47] doc: improve javadoc for context class --- .../context/RoCrateMetadataContext.java | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java index c4f5a8f2..d76cf86b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java @@ -39,7 +39,7 @@ public class RoCrateMetadataContext implements CrateMetadataContext { protected final HashMap other = new HashMap<>(); /** - * Default constructor for the creation of the default context. + * Default constructor for the creation of the v1.1 default context. */ public RoCrateMetadataContext() { this.addToContextFromUrl(DEFAULT_CONTEXT); @@ -47,6 +47,8 @@ public RoCrateMetadataContext() { /** * Constructor for creating the context from a list of url. + *

+ * Note: Does NOT contain the default context if not explicitly given! * * @param urls the url list with different context. */ @@ -56,6 +58,8 @@ public RoCrateMetadataContext(Collection urls) { /** * Constructor for creating the context from a json object. + *

+ * Note: Does NOT contain the default context if not explicitly given! * * @param context the Json object of the context. */ @@ -84,6 +88,28 @@ public RoCrateMetadataContext(JsonNode context) { } } + /** + * Converts the context into a JSON-LD representation. + *

+ * The resulting JSON structure depends on the content: + * - If there's only one URL and no key-value pairs: {"@context": "url"} + * - If there are multiple URLs and/or key-value pairs: {"@context": ["url1", "url2", {"key1": "value1", "key2": "value2"}]} + *

+ * Example output: + *

+   * {
+   *   "@context": [
+   *     "https://w3id.org/ro/crate/1.1/context",
+   *     {
+   *       "schema": "http://schema.org/",
+   *       "rdfs": "http://www.w3.org/2000/01/rdf-schema#"
+   *     }
+   *   ]
+   * }
+   * 
+ * + * @return an ObjectNode containing the JSON-LD context representation + */ @Override public ObjectNode getContextJsonEntity() { ObjectMapper objectMapper = MyObjectMapper.getMapper(); @@ -107,6 +133,21 @@ public ObjectNode getContextJsonEntity() { return finalNode; } + /** + * Checks if the given entity is valid according to the context. + *

+ * - full URLs in the @type and field names are considered valid without further checks. + * - The "@id" value is treated as a special case, where it refers to the entity's ID. + * - The "@json" type is a linked data built-in type and is always considered valid. + * - If a type or field name is not found in the context, it will print an error message and return false. + * - This method checks both the types in the @type array and the field names in the entity's properties. + * - Prefixes in the context are considered valid if they match the context keys. + * - Suffixes after a valid prefix are considered valid in any case. This is not perfect, + * but it would be hard to handle correctly. + * + * @param entity the entity to check + * @return true if the entity is valid, false otherwise + */ @Override public boolean checkEntity(AbstractEntity entity) { ObjectMapper objectMapper = MyObjectMapper.getMapper(); @@ -161,6 +202,13 @@ public boolean checkEntity(AbstractEntity entity) { return true; } + /** + * Adds a URL to the context. + *

+ * It will try to fetch the context from the URL. + * + * @param url the URL to add + */ @Override public void addToContextFromUrl(String url) { this.urls.add(url); @@ -200,18 +248,31 @@ public void addToContextFromUrl(String url) { })); } + /** + * Adds a key-value pair to the context. + * + * @param key the key to add. It may be a prefix or a term. + * @param value the value to add + */ @Override public void addToContext(String key, String value) { this.contextMap.put(key, value); this.other.put(key, value); } + /** + * @param key the key for the value to retrieve. + * @return the value of the key if it exists in the context, null otherwise. + */ @Override public String getValueOf(String key) { return Optional.ofNullable(this.contextMap.get(key)) .orElseGet(() -> this.other.get(key)); } + /** + * @return the set of all keys in the context. + */ @Override public Set getKeys() { List merged = new ArrayList<>(); @@ -220,6 +281,11 @@ public Set getKeys() { return Set.copyOf(merged); } + /** + * @return a map of all key-value pairs in the context. Note that some pairs may come + * from URLs or a pair may not be available as a context was not successfully resolved + * from a URL. + */ @Override public Map getPairs() { Map merged = new HashMap<>(); @@ -228,13 +294,18 @@ public Map getPairs() { return Map.copyOf(merged); } - + /** + * @param key the key to delete from the context. + */ @Override public void deleteValuePairFromContext(String key) { this.contextMap.remove(key); this.other.remove(key); } + /** + * @param url the URL to delete from the context. + */ @Override public void deleteUrlFromContext(String url) { this.urls.remove(url); From 11fc59c6dc58753c200bb02805697023fd6c0162 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 15:20:26 +0200 Subject: [PATCH 10/47] test: add rough tests for future RO-Crate metadata generation --- .../writer/RoCrateMetadataGenerationTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java new file mode 100644 index 00000000..e77572a5 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -0,0 +1,52 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.RoCrate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import static org.junit.jupiter.api.Assertions.*; + +class RoCrateMetadataGenerationTest { + + @Test + void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path tempDir) throws IOException { + // Create an empty RO-Crate + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + + // Write it to a temporary directory + Path outputPath = tempDir.resolve("test-crate"); + FolderWriter writer = new FolderWriter(); + writer.write(crate, outputPath); + + // Read the metadata file + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + + // Verify ro-crate-java entity exists + assertTrue(metadata.contains("\"@id\": \"#ro-crate-java\"")); + assertTrue(metadata.contains("\"@type\": \"SoftwareApplication\"")); + + // Verify write action exists + assertTrue(metadata.contains("\"@type\": \"CreateAction\"")); + assertTrue(metadata.contains("startTime")); + assertTrue(metadata.contains("agent")); + } + + @Test + void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir Path tempDir) throws IOException { + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + FolderWriter writer = new FolderWriter(); + writer.write(crate, outputPath); + + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + + // Test essential properties of the ro-crate-java entity + assertTrue(metadata.contains("\"name\": \"ro-crate-java\"")); + assertTrue(metadata.contains("\"url\": \"https://github.com/kit-data-manager/ro-crate-java\"")); + assertTrue(metadata.contains("\"version\"")); + } +} From b594997777e5c92a333f6aa5e094d9631efa723a Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 15:29:17 +0200 Subject: [PATCH 11/47] test: refine first tests for future RO-Crate metadata generation --- .../writer/RoCrateMetadataGenerationTest.java | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index e77572a5..928076c3 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -1,5 +1,7 @@ package edu.kit.datamanager.ro_crate.writer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.ro_crate.RoCrate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -7,46 +9,73 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; import static org.junit.jupiter.api.Assertions.*; class RoCrateMetadataGenerationTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + @Test void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path tempDir) throws IOException { - // Create an empty RO-Crate + // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); - - // Write it to a temporary directory Path outputPath = tempDir.resolve("test-crate"); - FolderWriter writer = new FolderWriter(); - writer.write(crate, outputPath); + Writers.newFolderWriter().save(crate, outputPath.toString()); - // Read the metadata file - String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode graph = rootNode.get("@graph"); - // Verify ro-crate-java entity exists - assertTrue(metadata.contains("\"@id\": \"#ro-crate-java\"")); - assertTrue(metadata.contains("\"@type\": \"SoftwareApplication\"")); + // Find ro-crate-java entity + JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); + assertEquals("SoftwareApplication", roCrateJavaEntity.get("@type").asText(), + "ro-crate-java should be of type SoftwareApplication"); - // Verify write action exists - assertTrue(metadata.contains("\"@type\": \"CreateAction\"")); - assertTrue(metadata.contains("startTime")); - assertTrue(metadata.contains("agent")); + // Find CreateAction entity + JsonNode createActionEntity = findEntityByType(graph, "CreateAction"); + assertNotNull(createActionEntity, "CreateAction entity should exist"); + assertNotNull(createActionEntity.get("startTime"), "CreateAction should have startTime"); + assertEquals("#ro-crate-java", createActionEntity.get("agent").get("@id").asText(), + "CreateAction should reference ro-crate-java as agent"); } @Test void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir Path tempDir) throws IOException { + // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); Path outputPath = tempDir.resolve("test-crate"); - FolderWriter writer = new FolderWriter(); - writer.write(crate, outputPath); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); - String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); + assertEquals("ro-crate-java", roCrateJavaEntity.get("name").asText(), + "should have correct name"); + assertEquals("https://github.com/kit-data-manager/ro-crate-java", + roCrateJavaEntity.get("url").asText(), + "should have correct repository URL"); + assertNotNull(roCrateJavaEntity.get("version"), + "should have version property"); + } + + private JsonNode findEntityById(JsonNode graph, String id) { + for (JsonNode entity : graph) { + if (entity.has("@id") && entity.get("@id").asText().equals(id)) { + return entity; + } + } + return null; + } - // Test essential properties of the ro-crate-java entity - assertTrue(metadata.contains("\"name\": \"ro-crate-java\"")); - assertTrue(metadata.contains("\"url\": \"https://github.com/kit-data-manager/ro-crate-java\"")); - assertTrue(metadata.contains("\"version\"")); + private JsonNode findEntityByType(JsonNode graph, String type) { + for (JsonNode entity : graph) { + if (entity.has("@type") && entity.get("@type").asText().equals(type)) { + return entity; + } + } + return null; } } From 5e5a37cd9411edee2324fbf03f4329d28bee2784 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 15:38:03 +0200 Subject: [PATCH 12/47] test: add bidirectional relation test for implicit crate metadata --- .../writer/RoCrateMetadataGenerationTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 928076c3..7c10a09c 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -61,6 +61,37 @@ void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir "should have version property"); } + @Test + void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir Path tempDir) throws IOException { + // Create and write crate + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode graph = rootNode.get("@graph"); + + // Get both entities + JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + JsonNode createActionEntity = findEntityByType(graph, "CreateAction"); + + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); + assertNotNull(createActionEntity, "CreateAction entity should exist"); + + // Test CreateAction -> ro-crate-java reference + JsonNode agentRef = createActionEntity.get("agent"); + assertNotNull(agentRef, "CreateAction should have agent property"); + assertEquals("#ro-crate-java", agentRef.get("@id").asText(), + "CreateAction's agent should reference ro-crate-java"); + + // Test ro-crate-java -> CreateAction reference + JsonNode actionRef = roCrateJavaEntity.get("action"); + assertNotNull(actionRef, "ro-crate-java should have action property"); + assertEquals(createActionEntity.get("@id").asText(), actionRef.get("@id").asText(), + "ro-crate-java's action should reference the CreateAction"); + } + private JsonNode findEntityById(JsonNode graph, String id) { for (JsonNode entity : graph) { if (entity.has("@id") && entity.get("@id").asText().equals(id)) { From bb170bb3182a27e82e0c029b235de12f70733e66 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 15:46:28 +0200 Subject: [PATCH 13/47] test: add test for accumulating actions during multiple writes to RO-Crate --- .../writer/RoCrateMetadataGenerationTest.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 7c10a09c..4a02db64 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -9,6 +9,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.StreamSupport; + import static org.junit.jupiter.api.Assertions.*; class RoCrateMetadataGenerationTest { @@ -92,6 +94,65 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P "ro-crate-java's action should reference the CreateAction"); } + @Test + void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) throws IOException { + // Create and write crate first time + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Write same crate two more times to simulate updates + Writers.newFolderWriter().save(crate, outputPath.toString()); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Parse final metadata file + JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode graph = rootNode.get("@graph"); + + // Get ro-crate-java entity + JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); + + // Verify actions array exists and has three entries + assertTrue(roCrateJavaEntity.get("action").isArray(), + "ro-crate-java should have an array of actions"); + assertEquals(3, roCrateJavaEntity.get("action").size(), + "should have three actions after three writes"); + + // Find all action entities + JsonNode createAction = findEntityByType(graph, "CreateAction"); + assertNotNull(createAction, "should have one CreateAction"); + + JsonNode[] createActions = findEntitiesByType(graph, "UpdateAction"); + assertEquals(1, createActions.length, "should have exactly one CreateAction"); + + JsonNode[] updateActions = findEntitiesByType(graph, "UpdateAction"); + assertEquals(2, updateActions.length, "should have two UpdateActions"); + + // Verify CreateAction properties + assertNotNull(createAction.get("startTime"), "CreateAction should have startTime"); + assertEquals("#ro-crate-java", createAction.get("agent").get("@id").asText(), + "CreateAction should reference ro-crate-java as agent"); + + // Verify UpdateAction properties + for (JsonNode updateAction : updateActions) { + assertNotNull(updateAction.get("startTime"), + "UpdateAction should have startTime"); + assertEquals("#ro-crate-java", updateAction.get("agent").get("@id").asText(), + "UpdateAction should reference ro-crate-java as agent"); + } + + // Verify chronological order of timestamps + String createTime = createAction.get("startTime").asText(); + String updateTime1 = updateActions[0].get("startTime").asText(); + String updateTime2 = updateActions[1].get("startTime").asText(); + + assertTrue(createTime.compareTo(updateTime1) < 0, + "First update should be after creation"); + assertTrue(updateTime1.compareTo(updateTime2) < 0, + "Second update should be after first update"); + } + private JsonNode findEntityById(JsonNode graph, String id) { for (JsonNode entity : graph) { if (entity.has("@id") && entity.get("@id").asText().equals(id)) { @@ -109,4 +170,10 @@ private JsonNode findEntityByType(JsonNode graph, String type) { } return null; } + + private JsonNode[] findEntitiesByType(JsonNode graph, String type) { + return StreamSupport.stream(graph.spliterator(), false) + .filter(entity -> entity.has("@type") && entity.get("@type").asText().equals(type)) + .toArray(JsonNode[]::new); + } } From 16002a6ee35bbc6c5b3af86a27c0aeb2050effb4 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 15:57:52 +0200 Subject: [PATCH 14/47] test: add debugging output for metadata in RO-Crate tests --- .../writer/RoCrateMetadataGenerationTest.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 4a02db64..504257b7 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.HelpFunctions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -24,8 +25,12 @@ void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path temp Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + // Parse metadata file - JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode rootNode = objectMapper.readTree(metadata); JsonNode graph = rootNode.get("@graph"); // Find ro-crate-java entity @@ -49,8 +54,12 @@ void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + // Parse metadata file - JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode rootNode = objectMapper.readTree(metadata); JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); @@ -70,8 +79,12 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + // Parse metadata file - JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + JsonNode rootNode = objectMapper.readTree(metadata); JsonNode graph = rootNode.get("@graph"); // Get both entities @@ -105,8 +118,12 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t Writers.newFolderWriter().save(crate, outputPath.toString()); Writers.newFolderWriter().save(crate, outputPath.toString()); - // Parse final metadata file - JsonNode rootNode = objectMapper.readTree(outputPath.resolve("ro-crate-metadata.json").toFile()); + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(metadata); JsonNode graph = rootNode.get("@graph"); // Get ro-crate-java entity From a08c31473e625bff03d77e51eef5be3a8d9987f8 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 16:34:18 +0200 Subject: [PATCH 15/47] test: add validation tests for RO-Crate version format and completeness --- .../writer/RoCrateMetadataGenerationTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 504257b7..36501855 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -170,6 +170,76 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t "Second update should be after first update"); } + @Test + void should_HaveValidVersionFormat_When_WritingCrate(@TempDir Path tempDir) throws IOException { + // Create and write crate + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(metadata); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); + + // Version format validation + @SuppressWarnings("DataFlowIssue") + String version = roCrateJavaEntity.get("version").asText(); + + // Semantic versioning regex pattern that allows: + // - Required: major.minor.patch (e.g., 1.2.3) + // - Optional: pre-release identifier (e.g., -rc1, -RC1, -beta.1, -SNAPSHOT) + // - Optional: build metadata (e.g., +build.123) + String semverPattern = "(?i)^\\d+\\.\\d+\\.\\d+(?:-(?:rc\\d+|alpha|beta|snapshot)(?:\\.\\d+)?)?(?:\\+[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*)?$"; + + assertTrue(version.matches(semverPattern), + String.format("Version '%s' should match semantic versioning format: major.minor.patch[-prerelease][+build]%n" + + "Examples: 1.2.3, 1.2.3-rc1, 1.2.3-SNAPSHOT, 1.2.3-beta.1, 1.2.3+build.123", version)); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void should_HaveCompleteMetadata_When_WritingCrate(@TempDir Path tempDir) throws IOException { + // Create and write crate + RoCrate crate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + Writers.newFolderWriter().save(crate, outputPath.toString()); + + // Read and print metadata for debugging + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonNode rootNode = objectMapper.readTree(metadata); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); + + // Required properties with specific values + assertEquals("ro-crate-java", roCrateJavaEntity.get("name").asText(), + "should have correct name"); + assertEquals("https://github.com/kit-data-manager/ro-crate-java", + roCrateJavaEntity.get("url").asText(), + "should have correct repository URL"); + assertEquals("SoftwareApplication", roCrateJavaEntity.get("@type").asText(), + "should have correct type"); + + // Optional but recommended properties + assertNotNull(roCrateJavaEntity.get("description"), + "should have a description"); + assertFalse(roCrateJavaEntity.get("description").asText().isEmpty(), + "description should not be empty"); + + assertNotNull(roCrateJavaEntity.get("license"), + "should have a license"); + assertTrue(roCrateJavaEntity.has("softwareVersion"), + "should have softwareVersion as an alias for version"); + assertEquals(roCrateJavaEntity.get("version").asText(), + roCrateJavaEntity.get("softwareVersion").asText(), + "version and softwareVersion should match"); + } + private JsonNode findEntityById(JsonNode graph, String id) { for (JsonNode entity : graph) { if (entity.has("@id") && entity.get("@id").asText().equals(id)) { From 4e817e96a0ae8b7b5f2b75f6d8cedc089b93c5fa Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 17:06:34 +0200 Subject: [PATCH 16/47] test: add provenance tests for modifying RO-Crate metadata --- .../writer/RoCrateMetadataGenerationTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 36501855..63df5ee5 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.reader.Readers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -240,6 +241,116 @@ void should_HaveCompleteMetadata_When_WritingCrate(@TempDir Path tempDir) throws "version and softwareVersion should match"); } + @Test + void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@TempDir Path tempDir) throws IOException { + // First create a crate without provenance information + RoCrate originalCrate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + + // Use writer with disabled provenance (not implemented yet) + Writers.newFolderWriter() + .withAutomaticProvenance(false) + .save(originalCrate, outputPath.toString()); + + // Verify the original crate has no provenance information + String originalMetadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(originalMetadata); + + JsonNode originalRoot = objectMapper.readTree(originalMetadata); + JsonNode originalGraph = originalRoot.get("@graph"); + assertNull(findEntityById(originalGraph, "#ro-crate-java"), + "Original crate should not have ro-crate-java entity"); + assertNull(findEntityByType(originalGraph, "CreateAction"), + "Original crate should not have CreateAction"); + assertNull(findEntityByType(originalGraph, "UpdateAction"), + "Original crate should not have UpdateAction"); + + // Now read and modify the crate + RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + modifiedCrate.getRootDataEntity().addProperty("description", "Modified crate"); + + // Write the modified crate with provenance enabled (default) + Path modifiedPath = tempDir.resolve("modified-crate"); + Writers.newFolderWriter().save(modifiedCrate, modifiedPath.toString()); + + // Read and verify the modified crate's metadata + String modifiedMetadata = Files.readString(modifiedPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(modifiedMetadata); + + JsonNode modifiedRoot = objectMapper.readTree(modifiedMetadata); + JsonNode modifiedGraph = modifiedRoot.get("@graph"); + + // Verify ro-crate-java entity was added + JsonNode roCrateJavaEntity = findEntityById(modifiedGraph, "#ro-crate-java"); + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should be added"); + + // Should only have UpdateAction, no CreateAction + assertNull(findEntityByType(modifiedGraph, "CreateAction"), + "Modified crate should not have CreateAction"); + + JsonNode updateAction = findEntityByType(modifiedGraph, "UpdateAction"); + assertNotNull(updateAction, "Should have UpdateAction"); + + // Verify update action properties + assertNotNull(updateAction.get("startTime"), + "UpdateAction should have startTime"); + assertEquals("#ro-crate-java", + updateAction.get("agent").get("@id").asText(), + "UpdateAction should reference ro-crate-java as agent"); + + // Verify ro-crate-java references the action + assertTrue(roCrateJavaEntity.get("action").isArray(), + "ro-crate-java should have an array of actions"); + assertEquals(1, roCrateJavaEntity.get("action").size(), + "should have exactly one action"); + assertEquals(updateAction.get("@id").asText(), + roCrateJavaEntity.get("action").get(0).get("@id").asText(), + "ro-crate-java should reference the UpdateAction"); + } + + @Test + void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir) throws IOException { + // First create a crate with normal provenance + RoCrate originalCrate = new RoCrate.RoCrateBuilder().build(); + Path outputPath = tempDir.resolve("test-crate"); + Writers.newFolderWriter().save(originalCrate, outputPath.toString()); + + // Now read and modify the crate + RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + modifiedCrate.getRootDataEntity().addProperty("description", "Modified crate"); + + // Write the modified crate + Writers.newFolderWriter().save(modifiedCrate, outputPath.toString()); + + // Read and verify the metadata + String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); + HelpFunctions.prettyPrintJsonString(metadata); + + JsonNode root = objectMapper.readTree(metadata); + JsonNode graph = root.get("@graph"); + + // Should have both CreateAction and UpdateAction + JsonNode createAction = findEntityByType(graph, "CreateAction"); + assertNotNull(createAction, "Original CreateAction should be preserved"); + + JsonNode[] updateActions = findEntitiesByType(graph, "UpdateAction"); + assertEquals(1, updateActions.length, "Should have exactly one UpdateAction"); + + // Verify chronological order + String createTime = createAction.get("startTime").asText(); + String updateTime = updateActions[0].get("startTime").asText(); + assertTrue(createTime.compareTo(updateTime) < 0, + "Update should be after creation"); + + // Verify ro-crate-java entity references both actions + JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + //noinspection DataFlowIssue + assertTrue(roCrateJavaEntity.get("action").isArray(), + "ro-crate-java should have an array of actions"); + assertEquals(2, roCrateJavaEntity.get("action").size(), + "should have both actions"); + } + private JsonNode findEntityById(JsonNode graph, String id) { for (JsonNode entity : graph) { if (entity.has("@id") && entity.get("@id").asText().equals(id)) { From b855b8086e56fcf960ad843f967bbf8dc6ca7343 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 17:21:42 +0200 Subject: [PATCH 17/47] test: add validation for RO-Crate metadata after writing and modifying implicitly or explicitly --- .../writer/RoCrateMetadataGenerationTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 63df5ee5..2e118520 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -5,6 +5,9 @@ import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.HelpFunctions; import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -18,14 +21,31 @@ class RoCrateMetadataGenerationTest { private final ObjectMapper objectMapper = new ObjectMapper(); + private Validator validator; + + @BeforeEach + void setUp() { + validator = new Validator(new JsonSchemaValidation()); + } + + private void validateCrate(RoCrate crate) { + assertTrue(validator.validate(crate), + "Crate should validate against the JSON schema"); + } @Test void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path tempDir) throws IOException { // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + // Re-read the crate to verify it's still valid after writing + RoCrate readCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + validateCrate(readCrate); + // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); HelpFunctions.prettyPrintJsonString(metadata); @@ -52,9 +72,14 @@ void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path temp void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir Path tempDir) throws IOException { // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + RoCrate readCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + validateCrate(readCrate); + // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); HelpFunctions.prettyPrintJsonString(metadata); @@ -77,9 +102,14 @@ void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir Path tempDir) throws IOException { // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + RoCrate readCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + validateCrate(readCrate); + // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); HelpFunctions.prettyPrintJsonString(metadata); @@ -112,12 +142,16 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) throws IOException { // Create and write crate first time RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); // Write same crate two more times to simulate updates Writers.newFolderWriter().save(crate, outputPath.toString()); Writers.newFolderWriter().save(crate, outputPath.toString()); + validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); @@ -175,8 +209,11 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t void should_HaveValidVersionFormat_When_WritingCrate(@TempDir Path tempDir) throws IOException { // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); @@ -206,8 +243,11 @@ void should_HaveValidVersionFormat_When_WritingCrate(@TempDir Path tempDir) thro void should_HaveCompleteMetadata_When_WritingCrate(@TempDir Path tempDir) throws IOException { // Create and write crate RoCrate crate = new RoCrate.RoCrateBuilder().build(); + validateCrate(crate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); + validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); @@ -245,6 +285,8 @@ void should_HaveCompleteMetadata_When_WritingCrate(@TempDir Path tempDir) throws void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@TempDir Path tempDir) throws IOException { // First create a crate without provenance information RoCrate originalCrate = new RoCrate.RoCrateBuilder().build(); + validateCrate(originalCrate); + Path outputPath = tempDir.resolve("test-crate"); // Use writer with disabled provenance (not implemented yet) @@ -267,7 +309,9 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp // Now read and modify the crate RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + validateCrate(modifiedCrate); modifiedCrate.getRootDataEntity().addProperty("description", "Modified crate"); + validateCrate(modifiedCrate); // Write the modified crate with provenance enabled (default) Path modifiedPath = tempDir.resolve("modified-crate"); @@ -312,12 +356,16 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir) throws IOException { // First create a crate with normal provenance RoCrate originalCrate = new RoCrate.RoCrateBuilder().build(); + validateCrate(originalCrate); + Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(originalCrate, outputPath.toString()); // Now read and modify the crate RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); + validateCrate(modifiedCrate); modifiedCrate.getRootDataEntity().addProperty("description", "Modified crate"); + validateCrate(modifiedCrate); // Write the modified crate Writers.newFolderWriter().save(modifiedCrate, outputPath.toString()); From b90d3c8e8a5186819804b9519cd8d3b2cc7d3a45 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 18:33:16 +0200 Subject: [PATCH 18/47] feat: add support for automatic provenance management in CrateWriter --- .../ro_crate/writer/CrateWriter.java | 9 ++ .../ro_crate/writer/ProvenanceManager.java | 98 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java index caba67f9..19ba4e3e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -15,11 +15,17 @@ public class CrateWriter { private final GenericWriterStrategy strategy; + protected boolean automaticProvenance = true; public CrateWriter(GenericWriterStrategy strategy) { this.strategy = strategy; } + public CrateWriter withAutomaticProvenance(boolean automaticProvenance) { + this.automaticProvenance = automaticProvenance; + return this; + } + /** * This method saves the crate to a destination provided. * @@ -29,6 +35,9 @@ public CrateWriter(GenericWriterStrategy strategy) { public void save(Crate crate, DESTINATION_TYPE destination) throws IOException { Validator defaultValidation = new Validator(new JsonSchemaValidation()); defaultValidation.validate(crate); + if (automaticProvenance) { + new ProvenanceManager().addProvenanceInformation(crate); + } this.strategy.save(crate, destination); } } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java new file mode 100644 index 00000000..bca47d7d --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -0,0 +1,98 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import java.time.Instant; +import java.util.Collection; +import java.util.Map; +import java.util.UUID; + +/** + * Manages provenance information for RO-Crates. + * Handles the creation and updating of ro-crate-java entity and its actions. + */ +class ProvenanceManager { + private static final String RO_CRATE_JAVA_ID = "#ro-crate-java"; + + void addProvenanceInformation(Crate crate) { + // Determine if this is the first write + boolean isFirstWrite = !crate.getJsonMetadata().contains(RO_CRATE_JAVA_ID); + + // Create action entity first + String actionId = "#" + UUID.randomUUID(); + ContextualEntity actionEntity = createActionEntity(actionId, isFirstWrite); + + // Create or update ro-crate-java entity + ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionId, isFirstWrite); + + // Add entities to crate in correct order (referenced entity first) + crate.addContextualEntity(roCrateJavaEntity); + crate.addContextualEntity(actionEntity); + } + + private ContextualEntity createActionEntity(String actionId, boolean isFirstWrite) { + return new ContextualEntity.ContextualEntityBuilder() + .setId(actionId) + .addType(isFirstWrite ? "CreateAction" : "UpdateAction") + .addProperty("startTime", Instant.now().toString()) + .addIdProperty("agent", RO_CRATE_JAVA_ID) + .build(); + } + + private ContextualEntity buildRoCrateJavaEntity(Crate crate, String newActionId, boolean isFirstWrite) { + ContextualEntity.ContextualEntityBuilder builder = new ContextualEntity.ContextualEntityBuilder() + .setId(RO_CRATE_JAVA_ID) + .addType("SoftwareApplication") + .addProperty("name", "ro-crate-java") + .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") + .addProperty("version", "1.0.0") + .addProperty("softwareVersion", "1.0.0") + .addProperty("license", "Apache-2.0") + .addProperty("description", "A Java library for creating and manipulating RO-Crates"); + + if (isFirstWrite) { + builder.addIdProperty("action", newActionId); + } else { + Collection entities = crate.getAllContextualEntities(); + for (ContextualEntity entity : entities) { + if (RO_CRATE_JAVA_ID.equals(entity.getId())) { + addActionToBuilder(builder, entity, newActionId); + break; + } + } + } + + return builder.build(); + } + + private void addActionToBuilder( + ContextualEntity.ContextualEntityBuilder builder, + ContextualEntity existingEntity, + String newActionId + ) { + Object existingAction = existingEntity.getProperty("action"); + if (existingAction == null) { + builder.addIdProperty("action", newActionId); + return; + } + + // When there are existing actions, we need to preserve them + if (existingAction instanceof Map) { + // Single previous action (as a Map containing @id) + String existingActionId = ((Map) existingAction).get("@id").toString(); + builder.addIdProperty("action", "#" + existingActionId); + builder.addIdProperty("action", newActionId); + } else if (existingAction instanceof Collection oldActions) { + // Multiple previous actions -> Add all existing actions + oldActions.stream() + .map(action -> ((Map) action).get("@id").toString()) + .map(id -> !id.startsWith("#") ? "#" + id : id) + .forEach(id -> builder.addIdProperty("action", id)); + // Add the new action + builder.addIdProperty("action", newActionId); + } else { + // Unexpected format, just add the new action + builder.addIdProperty("action", newActionId); + } + } +} From cc974440f950ece32dc94ed4d4ee7c83f5166408 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 3 Jun 2025 18:43:57 +0200 Subject: [PATCH 19/47] chore: add TODO for reading software version from Gradle in ProvenanceManager --- .../edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index bca47d7d..e58d8ecf 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -45,6 +45,7 @@ private ContextualEntity buildRoCrateJavaEntity(Crate crate, String newActionId, .addType("SoftwareApplication") .addProperty("name", "ro-crate-java") .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") + // TODO read software version and version from gradle (write into resources properties file when building and read it from there) .addProperty("version", "1.0.0") .addProperty("softwareVersion", "1.0.0") .addProperty("license", "Apache-2.0") From 2221627659d2f23e3e3d00834efa601e82a4a8bc Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 13:39:54 +0200 Subject: [PATCH 20/47] fix: standardize property name to 'Action' in ProvenanceManager and related tests --- .../ro_crate/writer/ProvenanceManager.java | 12 ++++++------ .../writer/RoCrateMetadataGenerationTest.java | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index e58d8ecf..cedd24eb 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -52,7 +52,7 @@ private ContextualEntity buildRoCrateJavaEntity(Crate crate, String newActionId, .addProperty("description", "A Java library for creating and manipulating RO-Crates"); if (isFirstWrite) { - builder.addIdProperty("action", newActionId); + builder.addIdProperty("Action", newActionId); } else { Collection entities = crate.getAllContextualEntities(); for (ContextualEntity entity : entities) { @@ -71,7 +71,7 @@ private void addActionToBuilder( ContextualEntity existingEntity, String newActionId ) { - Object existingAction = existingEntity.getProperty("action"); + Object existingAction = existingEntity.getProperty("Action"); if (existingAction == null) { builder.addIdProperty("action", newActionId); return; @@ -81,8 +81,8 @@ private void addActionToBuilder( if (existingAction instanceof Map) { // Single previous action (as a Map containing @id) String existingActionId = ((Map) existingAction).get("@id").toString(); - builder.addIdProperty("action", "#" + existingActionId); - builder.addIdProperty("action", newActionId); + builder.addIdProperty("Action", "#" + existingActionId); + builder.addIdProperty("Action", newActionId); } else if (existingAction instanceof Collection oldActions) { // Multiple previous actions -> Add all existing actions oldActions.stream() @@ -90,10 +90,10 @@ private void addActionToBuilder( .map(id -> !id.startsWith("#") ? "#" + id : id) .forEach(id -> builder.addIdProperty("action", id)); // Add the new action - builder.addIdProperty("action", newActionId); + builder.addIdProperty("Action", newActionId); } else { // Unexpected format, just add the new action - builder.addIdProperty("action", newActionId); + builder.addIdProperty("Action", newActionId); } } } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 2e118520..12ec25e6 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -132,7 +132,7 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P "CreateAction's agent should reference ro-crate-java"); // Test ro-crate-java -> CreateAction reference - JsonNode actionRef = roCrateJavaEntity.get("action"); + JsonNode actionRef = roCrateJavaEntity.get("Action"); assertNotNull(actionRef, "ro-crate-java should have action property"); assertEquals(createActionEntity.get("@id").asText(), actionRef.get("@id").asText(), "ro-crate-java's action should reference the CreateAction"); @@ -166,9 +166,9 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); // Verify actions array exists and has three entries - assertTrue(roCrateJavaEntity.get("action").isArray(), + assertTrue(roCrateJavaEntity.get("Action").isArray(), "ro-crate-java should have an array of actions"); - assertEquals(3, roCrateJavaEntity.get("action").size(), + assertEquals(3, roCrateJavaEntity.get("Action").size(), "should have three actions after three writes"); // Find all action entities @@ -343,12 +343,12 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp "UpdateAction should reference ro-crate-java as agent"); // Verify ro-crate-java references the action - assertTrue(roCrateJavaEntity.get("action").isArray(), + assertTrue(roCrateJavaEntity.get("Action").isArray(), "ro-crate-java should have an array of actions"); - assertEquals(1, roCrateJavaEntity.get("action").size(), + assertEquals(1, roCrateJavaEntity.get("Action").size(), "should have exactly one action"); assertEquals(updateAction.get("@id").asText(), - roCrateJavaEntity.get("action").get(0).get("@id").asText(), + roCrateJavaEntity.get("Action").get(0).get("@id").asText(), "ro-crate-java should reference the UpdateAction"); } @@ -393,9 +393,9 @@ void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir // Verify ro-crate-java entity references both actions JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); //noinspection DataFlowIssue - assertTrue(roCrateJavaEntity.get("action").isArray(), + assertTrue(roCrateJavaEntity.get("Action").isArray(), "ro-crate-java should have an array of actions"); - assertEquals(2, roCrateJavaEntity.get("action").size(), + assertEquals(2, roCrateJavaEntity.get("Action").size(), "should have both actions"); } From b36e64bb1b656f4ea8a2754dbdc584a9ec597d30 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 13:42:19 +0200 Subject: [PATCH 21/47] feat: add imported state management to Crate interface and implementations --- .../edu/kit/datamanager/ro_crate/Crate.java | 22 +++++++++++++++++++ .../edu/kit/datamanager/ro_crate/RoCrate.java | 18 +++++++++++++++ .../ro_crate/reader/CrateReader.java | 2 +- .../ro_crate/writer/ProvenanceManager.java | 2 +- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java b/src/main/java/edu/kit/datamanager/ro_crate/Crate.java index aff1b6fe..782ff601 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/Crate.java @@ -22,6 +22,28 @@ */ public interface Crate { + /** + * Mark the crate as imported, i.e. it has been read from a file + * or is for other reasons not considered a new crate. + *

+ * This is useful mostly for readers to indicate this in case + * the crate may not have any provenance information yet and + * should still be recognized as an imported crate. + * + * @return this crate, for convenience. + */ + Crate markAsImported(); + + /** + * Check if the crate is marked as imported. + *

+ * If true, it indicates that the crate has been read from a file + * or is for other reasons not considered a new crate. + * + * @return true if the crate is marked as imported, false otherwise. + */ + boolean isImported(); + /** * Read version from the crate descriptor and return it as a class * representation. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java index 32c22dec..8ebae01e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -50,6 +50,24 @@ public class RoCrate implements Crate { protected Collection untrackedFiles; + /** + * Indicates whether this crate has been imported from an external source. + * This is used to determine if ro-crate-java should add a CreateAction + * or an UpdateAction in the provenance on export. + */ + protected boolean isImported = false; + + @Override + public RoCrate markAsImported() { + this.isImported = true; + return this; + } + + @Override + public boolean isImported() { + return this.isImported; + } + @Override public CratePreview getPreview() { return this.roCratePreview; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java index 5132e44b..271725b5 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -97,7 +97,7 @@ public RoCrate readCrate(T location) throws IOException { usedFiles.add(files.toPath().resolve(FILE_METADATA_JSON).toFile().getPath()); usedFiles.add(files.toPath().resolve(FILE_PREVIEW_HTML).toFile().getPath()); usedFiles.add(files.toPath().resolve(FILE_PREVIEW_FILES).toFile().getPath()); - return rebuildCrate(metadataJson, files, usedFiles); + return rebuildCrate(metadataJson, files, usedFiles).markAsImported(); } private RoCrate rebuildCrate(ObjectNode metadataJson, File files, HashSet usedFiles) { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index cedd24eb..4108a907 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -16,7 +16,7 @@ class ProvenanceManager { void addProvenanceInformation(Crate crate) { // Determine if this is the first write - boolean isFirstWrite = !crate.getJsonMetadata().contains(RO_CRATE_JAVA_ID); + boolean isFirstWrite = !crate.getJsonMetadata().contains(RO_CRATE_JAVA_ID) && !crate.isImported(); // Create action entity first String actionId = "#" + UUID.randomUUID(); From fe63d12b3f6f0f0f3e40d9184c88dbb00c64fae2 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 14:21:56 +0200 Subject: [PATCH 22/47] feat: refactor buildRoCrateJavaEntity to streamline entity creation and action handling --- .../ro_crate/writer/ProvenanceManager.java | 84 ++++++------------- 1 file changed, 27 insertions(+), 57 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 4108a907..c7db90dd 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -7,6 +7,8 @@ import java.util.Map; import java.util.UUID; +import static edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; + /** * Manages provenance information for RO-Crates. * Handles the creation and updating of ro-crate-java entity and its actions. @@ -25,13 +27,13 @@ void addProvenanceInformation(Crate crate) { // Create or update ro-crate-java entity ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionId, isFirstWrite); - // Add entities to crate in correct order (referenced entity first) + // Add entities to crate crate.addContextualEntity(roCrateJavaEntity); crate.addContextualEntity(actionEntity); } private ContextualEntity createActionEntity(String actionId, boolean isFirstWrite) { - return new ContextualEntity.ContextualEntityBuilder() + return new ContextualEntityBuilder() .setId(actionId) .addType(isFirstWrite ? "CreateAction" : "UpdateAction") .addProperty("startTime", Instant.now().toString()) @@ -39,61 +41,29 @@ private ContextualEntity createActionEntity(String actionId, boolean isFirstWrit .build(); } - private ContextualEntity buildRoCrateJavaEntity(Crate crate, String newActionId, boolean isFirstWrite) { - ContextualEntity.ContextualEntityBuilder builder = new ContextualEntity.ContextualEntityBuilder() - .setId(RO_CRATE_JAVA_ID) - .addType("SoftwareApplication") - .addProperty("name", "ro-crate-java") - .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") - // TODO read software version and version from gradle (write into resources properties file when building and read it from there) - .addProperty("version", "1.0.0") - .addProperty("softwareVersion", "1.0.0") - .addProperty("license", "Apache-2.0") - .addProperty("description", "A Java library for creating and manipulating RO-Crates"); - - if (isFirstWrite) { - builder.addIdProperty("Action", newActionId); - } else { - Collection entities = crate.getAllContextualEntities(); - for (ContextualEntity entity : entities) { - if (RO_CRATE_JAVA_ID.equals(entity.getId())) { - addActionToBuilder(builder, entity, newActionId); - break; - } - } - } - - return builder.build(); - } - - private void addActionToBuilder( - ContextualEntity.ContextualEntityBuilder builder, - ContextualEntity existingEntity, - String newActionId + private ContextualEntity buildRoCrateJavaEntity( + Crate crate, + String newActionId, + boolean isFirstWrite ) { - Object existingAction = existingEntity.getProperty("Action"); - if (existingAction == null) { - builder.addIdProperty("action", newActionId); - return; - } - - // When there are existing actions, we need to preserve them - if (existingAction instanceof Map) { - // Single previous action (as a Map containing @id) - String existingActionId = ((Map) existingAction).get("@id").toString(); - builder.addIdProperty("Action", "#" + existingActionId); - builder.addIdProperty("Action", newActionId); - } else if (existingAction instanceof Collection oldActions) { - // Multiple previous actions -> Add all existing actions - oldActions.stream() - .map(action -> ((Map) action).get("@id").toString()) - .map(id -> !id.startsWith("#") ? "#" + id : id) - .forEach(id -> builder.addIdProperty("action", id)); - // Add the new action - builder.addIdProperty("Action", newActionId); - } else { - // Unexpected format, just add the new action - builder.addIdProperty("Action", newActionId); - } + ContextualEntity self = crate.getAllContextualEntities().stream() + .filter(contextualEntity -> RO_CRATE_JAVA_ID.equals(contextualEntity.getId())) + .findFirst() + .orElseGet(() -> + new ContextualEntityBuilder() + .setId(RO_CRATE_JAVA_ID) + .addType("SoftwareApplication") + .addProperty("name", "ro-crate-java") + .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") + // TODO read software version and version from gradle (write into resources properties file when building and read it from there) + .addProperty("version", "1.0.0") + .addProperty("softwareVersion", "1.0.0") + .addProperty("license", "Apache-2.0") + .addProperty("description", "A Java library for creating and manipulating RO-Crates") + .addIdProperty("Action", newActionId) + .build() + ); + self.addIdProperty("Action", newActionId); + return self; } } From f2086f3b74cdd518b3cfa9ca03ac9d582c6947df Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 14:40:11 +0200 Subject: [PATCH 23/47] fix: wrong assumptions / typos in RoCrateMetadataGenerationTest --- .../writer/RoCrateMetadataGenerationTest.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 12ec25e6..6a390de9 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -175,7 +175,7 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t JsonNode createAction = findEntityByType(graph, "CreateAction"); assertNotNull(createAction, "should have one CreateAction"); - JsonNode[] createActions = findEntitiesByType(graph, "UpdateAction"); + JsonNode[] createActions = findEntitiesByType(graph, "CreateAction"); assertEquals(1, createActions.length, "should have exactly one CreateAction"); JsonNode[] updateActions = findEntitiesByType(graph, "UpdateAction"); @@ -198,11 +198,12 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t String createTime = createAction.get("startTime").asText(); String updateTime1 = updateActions[0].get("startTime").asText(); String updateTime2 = updateActions[1].get("startTime").asText(); - + // The order of updates is not the order in the graph. + // But we can check that creation happened before the updates: assertTrue(createTime.compareTo(updateTime1) < 0, "First update should be after creation"); - assertTrue(updateTime1.compareTo(updateTime2) < 0, - "Second update should be after first update"); + assertTrue(createTime.compareTo(updateTime2) < 0, + "Second update should be after creation"); } @Test @@ -343,12 +344,10 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp "UpdateAction should reference ro-crate-java as agent"); // Verify ro-crate-java references the action - assertTrue(roCrateJavaEntity.get("Action").isArray(), - "ro-crate-java should have an array of actions"); - assertEquals(1, roCrateJavaEntity.get("Action").size(), - "should have exactly one action"); + assertTrue(roCrateJavaEntity.get("Action").isObject(), + "ro-crate-java should have a single reference to an UpdateAction"); assertEquals(updateAction.get("@id").asText(), - roCrateJavaEntity.get("Action").get(0).get("@id").asText(), + roCrateJavaEntity.get("Action").get("@id").asText(), "ro-crate-java should reference the UpdateAction"); } From 7d3020c60bf50f5c8b82c7f27e79f759e1408ad5 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 15:47:09 +0200 Subject: [PATCH 24/47] fix(test): disable automatic provenance in tests unrelated to metadata creation so that checks stay as simple as possible --- .../kit/datamanager/ro_crate/preview/CratePreview.java | 2 ++ .../kit/datamanager/ro_crate/reader/CommonReaderTest.java | 8 ++++++-- .../kit/datamanager/ro_crate/reader/FolderReaderTest.java | 4 +++- .../kit/datamanager/ro_crate/reader/ZipReaderTest.java | 4 +++- .../datamanager/ro_crate/reader/ZipStreamReaderTest.java | 4 +++- .../kit/datamanager/ro_crate/writer/FolderWriterTest.java | 1 + .../ro_crate/writer/RoCrateWriterSpec12Test.java | 1 + .../datamanager/ro_crate/writer/ZipStreamWriterTest.java | 6 +++++- .../kit/datamanager/ro_crate/writer/ZipWriterTest.java | 3 +++ 9 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java index 2459f8b5..6f76e708 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java @@ -40,6 +40,8 @@ default void generate(Crate crate, File targetDir) throws IOException { // as this is usually called in the process of writing a crate // (including preview) new CrateWriter<>(new WriteFolderStrategy().disablePreview()) + // We assume the caller (e.g. a writer) already stored the provenance. + .withAutomaticProvenance(false) .save(crate, targetDir.getAbsolutePath()); this.saveAllToFolder(targetDir); try (var stream = Files.list(targetDir.toPath())) { diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java index 1e162a1c..19bcf61c 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java @@ -127,8 +127,12 @@ default void TestWithFileWithLocation(@TempDir Path temp) throws IOException { { // write raw crate and imported crate to two different directories CrateWriter writer = Writers.newFolderWriter(); - writer.save(rawCrate, rawCrateTarget.toString()); - writer.save(importedCrate, importedCrateTarget.toString()); + writer + .withAutomaticProvenance(false) + .save(rawCrate, rawCrateTarget.toString()); + writer + .withAutomaticProvenance(false) + .save(importedCrate, importedCrateTarget.toString()); } assertTrue(HelpFunctions.compareTwoDir(rawCrateTarget.toFile(), importedCrateTarget.toFile())); diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java index 83bf4f78..16e51ff9 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java @@ -21,7 +21,9 @@ class FolderReaderTest implements CommonReaderTest { @Override public void saveCrate(Crate crate, Path target) throws IOException { - Writers.newFolderWriter().save(crate, target.toAbsolutePath().toString()); + Writers.newFolderWriter() + .withAutomaticProvenance(false) + .save(crate, target.toAbsolutePath().toString()); assertTrue(target.toFile().isDirectory()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java index 35b168e1..336b2a67 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java @@ -14,7 +14,9 @@ class ZipReaderTest implements { @Override public void saveCrate(Crate crate, Path target) throws IOException { - Writers.newZipPathWriter().save(crate, target.toAbsolutePath().toString()); + Writers.newZipPathWriter() + .withAutomaticProvenance(false) + .save(crate, target.toAbsolutePath().toString()); assertTrue(target.toFile().isFile()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java index 11b82d12..c7acb975 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java @@ -33,7 +33,9 @@ public void saveCrate(Crate crate, Path target) throws IOException { try ( FileOutputStream fos = new FileOutputStream(target_file) ) { - Writers.newZipStreamWriter().save(crate, fos); + Writers.newZipStreamWriter() + .withAutomaticProvenance(false) + .save(crate, fos); } assertTrue(target_file.isFile()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java index 0c187029..06fb85b7 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java @@ -16,6 +16,7 @@ class FolderWriterTest implements CommonWriterTest { @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newFolderWriter() + .withAutomaticProvenance(false) .save(crate, target.toAbsolutePath().toString()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java index 90161e4d..5b6b2d92 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java @@ -30,6 +30,7 @@ void writeDoesNotModifyTest(@TempDir Path tempDir) throws IOException, URISyntax Path targetDir = tempDir.resolve("spec12writeUnmodified"); Writers.newFolderWriter() + .withAutomaticProvenance(false) .save(crate, targetDir.toAbsolutePath().toString()); // compare directories diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java index 83e311b1..256fcd5b 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java @@ -17,7 +17,9 @@ class ZipStreamWriterTest implements @Override public void saveCrate(Crate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { - Writers.newZipStreamWriter().save(crate, stream); + Writers.newZipStreamWriter() + .withAutomaticProvenance(false) + .save(crate, stream); } } @@ -25,6 +27,7 @@ public void saveCrate(Crate crate, Path target) throws IOException { public void saveCrateElnStyle(Crate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { new CrateWriter<>(new WriteZipStreamStrategy().usingElnStyle()) + .withAutomaticProvenance(false) .save(crate, stream); } } @@ -33,6 +36,7 @@ public void saveCrateElnStyle(Crate crate, Path target) throws IOException { public void saveCrateSubdirectoryStyle(RoCrate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { new CrateWriter<>(new WriteZipStreamStrategy().withRootSubdirectory()) + .withAutomaticProvenance(false) .save(crate, stream); } } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java index bfb29c3d..251072fb 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java @@ -13,18 +13,21 @@ class ZipWriterTest implements @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newZipPathWriter() + .withAutomaticProvenance(false) .save(crate, target.toAbsolutePath().toString()); } @Override public void saveCrateElnStyle(Crate crate, Path target) throws IOException { new CrateWriter<>(new WriteZipStrategy().usingElnStyle()) + .withAutomaticProvenance(false) .save(crate, target.toAbsolutePath().toString()); } @Override public void saveCrateSubdirectoryStyle(RoCrate crate, Path target) throws IOException { new CrateWriter<>(new WriteZipStrategy().withRootSubdirectory()) + .withAutomaticProvenance(false) .save(crate, target.toString()); } } From 72573543a4b0c88b6af5c2f55f41ce3fd95d8bda Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 16:48:08 +0200 Subject: [PATCH 25/47] feat: add version properties generation and load version from properties file in ProvenanceManager --- build.gradle | 21 ++++++++ .../ro_crate/writer/ProvenanceManager.java | 54 +++++++++++++------ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 8d162355..64b50abd 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,22 @@ configurations { performanceTestImplementation.extendsFrom implementation } +// Task for creating a resource file with the version info +tasks.register("generateVersionProps", WriteProperties) { t -> + def generatedResourcesDir = project.layout.buildDirectory.dir(["resources", "main"].join(File.separator)) + def outputFile = generatedResourcesDir.map { it.file("version.properties") } + + t.destinationFile = outputFile.get().asFile + t.property("version", version) +} + +tasks.register("generateVersionPropsTest", WriteProperties) { t -> + def generatedResourcesDir = project.layout.buildDirectory.dir(["resources", "test"].join(File.separator)) + def outputFile = generatedResourcesDir.map { it.file("version.properties") } + + t.destinationFile = outputFile.get().asFile + t.property("version", version) +} tasks.register('performanceContextEntitiesBenchmark', JavaExec) { description = "Run the context entities benchmarks." @@ -154,8 +170,13 @@ tasks.register('performanceReadWriteMultipleCratesBenchmark', JavaExec) { mainClass = 'edu.kit.datamanager.ro_crate.multiplecrates.MultipleCratesWriteAndRead' } +compileJava { + dependsOn generateVersionProps +} + test { useJUnitPlatform() + dependsOn generateVersionPropsTest finalizedBy jacocoTestReport } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index c7db90dd..3189bd11 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -2,9 +2,12 @@ import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.time.Instant; -import java.util.Collection; -import java.util.Map; +import java.util.Properties; import java.util.UUID; import static edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; @@ -49,21 +52,42 @@ private ContextualEntity buildRoCrateJavaEntity( ContextualEntity self = crate.getAllContextualEntities().stream() .filter(contextualEntity -> RO_CRATE_JAVA_ID.equals(contextualEntity.getId())) .findFirst() - .orElseGet(() -> - new ContextualEntityBuilder() - .setId(RO_CRATE_JAVA_ID) - .addType("SoftwareApplication") - .addProperty("name", "ro-crate-java") - .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") - // TODO read software version and version from gradle (write into resources properties file when building and read it from there) - .addProperty("version", "1.0.0") - .addProperty("softwareVersion", "1.0.0") - .addProperty("license", "Apache-2.0") - .addProperty("description", "A Java library for creating and manipulating RO-Crates") - .addIdProperty("Action", newActionId) - .build() + .orElseGet(() -> { + String version = loadVersionFromProperties(); + return new ContextualEntityBuilder() + .setId(RO_CRATE_JAVA_ID) + .addType("SoftwareApplication") + .addProperty("name", "ro-crate-java") + .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") + // TODO read software version and version from gradle (write into resources properties file when building and read it from there) + .addProperty("version", version) + .addProperty("softwareVersion", version) + .addProperty("license", "Apache-2.0") + .addProperty("description", "A Java library for creating and manipulating RO-Crates") + .addIdProperty("Action", newActionId) + .build(); + } ); self.addIdProperty("Action", newActionId); return self; } + + private String loadVersionFromProperties() { + try { + URL resource = this.getClass().getResource("/version.properties"); + if (resource != null) { + try (InputStream input = resource.openStream()) { + Properties properties = new Properties(); + properties.load(input); + return properties.getProperty("version"); + } + } else { + System.err.println("Properties file not found!"); + return "unknown"; + } + } catch (IOException e) { + System.err.println("Properties file not found!"); + return "unknown"; + } + } } From e60ae891570e9b48bb430e84d06d2e64210b1bcd Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 4 Jun 2025 16:48:22 +0200 Subject: [PATCH 26/47] chore: update version to 2.1.0-rc3 in CITATION.cff --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index bfc3a960..c92f7d15 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,6 +10,6 @@ authors: given-names: "Sabrine" orcid: "https://orcid.org/0000-0002-4480-6116" title: "ro-crate-java" -version: 1.0.3 +version: 2.1.0-rc3 date-released: 2022-07-19 url: "https://github.com/kit-data-manager/ro-crate-java" From db566850bb2b30aa849299864bb46a61f5719373 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 13:12:04 +0200 Subject: [PATCH 27/47] fix: improve error handling in loadVersionFromProperties method --- .../ro_crate/writer/ProvenanceManager.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 3189bd11..0406a2d4 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -73,21 +73,21 @@ private ContextualEntity buildRoCrateJavaEntity( } private String loadVersionFromProperties() { - try { - URL resource = this.getClass().getResource("/version.properties"); - if (resource != null) { - try (InputStream input = resource.openStream()) { - Properties properties = new Properties(); - properties.load(input); - return properties.getProperty("version"); - } - } else { - System.err.println("Properties file not found!"); - return "unknown"; + URL resource = this.getClass().getResource("/version.properties"); + if (resource == null) { + throw new IllegalStateException("version.properties not found in classpath. This indicates a build configuration issue."); + } + + try (InputStream input = resource.openStream()) { + Properties properties = new Properties(); + properties.load(input); + String version = properties.getProperty("version"); + if (version == null || version.trim().isEmpty()) { + throw new IllegalStateException("No version property found in version.properties"); } + return version.trim(); } catch (IOException e) { - System.err.println("Properties file not found!"); - return "unknown"; + throw new IllegalStateException("Failed to read version from properties file", e); } } } From 501060d80a268044966e13646f4d82a7f98826ab Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 13:49:33 +0200 Subject: [PATCH 28/47] chore: implement ClasspathPropertiesVersionProvider for version retrieval and refactor ProvenanceManager to use it --- .../ClasspathPropertiesVersionProvider.java | 48 +++++++++++++++++++ .../ro_crate/util/VersionProvider.java | 10 ++++ .../ro_crate/writer/ProvenanceManager.java | 45 ++++++++--------- 3 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java new file mode 100644 index 00000000..88a5a837 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -0,0 +1,48 @@ +package edu.kit.datamanager.ro_crate.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + +public class ClasspathPropertiesVersionProvider implements VersionProvider { + private static final String PROPERTIES_FILE = "ro-crate-java.properties"; + private static final String VERSION_KEY = "version"; + + /** + * Cached version to avoid repeated file/resource reads. + */ + private String cachedVersion = null; + + /** + * Constructs a ClasspathPropertiesVersionProvider that reads the version from a properties file in the classpath. + */ + public ClasspathPropertiesVersionProvider() { + this.cachedVersion = getVersion(); + } + + @Override + public String getVersion() { + if (cachedVersion != null) { + return cachedVersion; + } + + URL resource = this.getClass().getResource("/version.properties"); + if (resource == null) { + throw new IllegalStateException( + "version.properties not found in classpath. This indicates a build configuration issue."); + } + + try (InputStream input = resource.openStream()) { + Properties properties = new Properties(); + properties.load(input); + String version = properties.getProperty("version"); + if (version == null || version.trim().isEmpty()) { + throw new IllegalStateException("No version property found in version.properties"); + } + return version.trim(); + } catch (IOException e) { + throw new IllegalStateException("Failed to read version from properties file", e); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java new file mode 100644 index 00000000..cd5c4ad2 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java @@ -0,0 +1,10 @@ +package edu.kit.datamanager.ro_crate.util; + +public interface VersionProvider { + /** + * Returns the version of the ro-crate-java library. + * + * @return The version string. + */ + String getVersion(); +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 0406a2d4..6922e834 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -2,12 +2,10 @@ import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.util.ClasspathPropertiesVersionProvider; +import edu.kit.datamanager.ro_crate.util.VersionProvider; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.time.Instant; -import java.util.Properties; import java.util.UUID; import static edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; @@ -19,6 +17,24 @@ class ProvenanceManager { private static final String RO_CRATE_JAVA_ID = "#ro-crate-java"; + protected VersionProvider versionProvider; + + /** + * Constructs a ProvenanceManager with the default ClasspathPropertiesVersionProvider. + */ + public ProvenanceManager() { + this(new ClasspathPropertiesVersionProvider()); + } + + /** + * Constructs a ProvenanceManager with a specified VersionProvider. + * + * @param versionProvider The VersionProvider to use for retrieving the version of ro-crate-java. + */ + public ProvenanceManager(VersionProvider versionProvider) { + this.versionProvider = versionProvider; + } + void addProvenanceInformation(Crate crate) { // Determine if this is the first write boolean isFirstWrite = !crate.getJsonMetadata().contains(RO_CRATE_JAVA_ID) && !crate.isImported(); @@ -53,7 +69,7 @@ private ContextualEntity buildRoCrateJavaEntity( .filter(contextualEntity -> RO_CRATE_JAVA_ID.equals(contextualEntity.getId())) .findFirst() .orElseGet(() -> { - String version = loadVersionFromProperties(); + String version = this.versionProvider.getVersion(); return new ContextualEntityBuilder() .setId(RO_CRATE_JAVA_ID) .addType("SoftwareApplication") @@ -71,23 +87,4 @@ private ContextualEntity buildRoCrateJavaEntity( self.addIdProperty("Action", newActionId); return self; } - - private String loadVersionFromProperties() { - URL resource = this.getClass().getResource("/version.properties"); - if (resource == null) { - throw new IllegalStateException("version.properties not found in classpath. This indicates a build configuration issue."); - } - - try (InputStream input = resource.openStream()) { - Properties properties = new Properties(); - properties.load(input); - String version = properties.getProperty("version"); - if (version == null || version.trim().isEmpty()) { - throw new IllegalStateException("No version property found in version.properties"); - } - return version.trim(); - } catch (IOException e) { - throw new IllegalStateException("Failed to read version from properties file", e); - } - } } From 55648fd333cd9a7427e4ed33efc2a20993928ac3 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 14:19:30 +0200 Subject: [PATCH 29/47] feat: create provenance entity per library version and keep old ones --- .../ro_crate/writer/ProvenanceManager.java | 43 +++++++++++++------ .../writer/RoCrateMetadataGenerationTest.java | 32 ++++++++------ 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 6922e834..2dd19d6a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -6,7 +6,6 @@ import edu.kit.datamanager.ro_crate.util.VersionProvider; import java.time.Instant; -import java.util.UUID; import static edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; @@ -15,7 +14,18 @@ * Handles the creation and updating of ro-crate-java entity and its actions. */ class ProvenanceManager { - private static final String RO_CRATE_JAVA_ID = "#ro-crate-java"; + private record IdPrefix(String prefix) { + public String withSuffix(String suffix) { + return prefix + "-" + suffix; + } + + @Override + public String toString() { + return prefix; + } + } + + private static final IdPrefix RO_CRATE_JAVA_ID = new IdPrefix("#ro-crate-java"); protected VersionProvider versionProvider; @@ -35,47 +45,52 @@ public ProvenanceManager(VersionProvider versionProvider) { this.versionProvider = versionProvider; } + public String getLibraryId() { + return RO_CRATE_JAVA_ID.withSuffix(versionProvider.getVersion().toLowerCase()); + } + void addProvenanceInformation(Crate crate) { // Determine if this is the first write - boolean isFirstWrite = !crate.getJsonMetadata().contains(RO_CRATE_JAVA_ID) && !crate.isImported(); + boolean isFirstWrite = crate.getAllContextualEntities().stream().noneMatch( + entity -> entity.getId().startsWith(RO_CRATE_JAVA_ID.toString())) + && !crate.isImported(); + + String libraryId = this.getLibraryId(); // Create action entity first - String actionId = "#" + UUID.randomUUID(); - ContextualEntity actionEntity = createActionEntity(actionId, isFirstWrite); + ContextualEntity actionEntity = createActionEntity(isFirstWrite, libraryId); // Create or update ro-crate-java entity - ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionId, isFirstWrite); + ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionEntity.getId(), libraryId); // Add entities to crate crate.addContextualEntity(roCrateJavaEntity); crate.addContextualEntity(actionEntity); } - private ContextualEntity createActionEntity(String actionId, boolean isFirstWrite) { + private ContextualEntity createActionEntity(boolean isFirstWrite, String libraryId) { return new ContextualEntityBuilder() - .setId(actionId) .addType(isFirstWrite ? "CreateAction" : "UpdateAction") .addProperty("startTime", Instant.now().toString()) - .addIdProperty("agent", RO_CRATE_JAVA_ID) + .addIdProperty("agent", libraryId) .build(); } private ContextualEntity buildRoCrateJavaEntity( Crate crate, String newActionId, - boolean isFirstWrite + String libraryId ) { + String version = this.versionProvider.getVersion(); ContextualEntity self = crate.getAllContextualEntities().stream() - .filter(contextualEntity -> RO_CRATE_JAVA_ID.equals(contextualEntity.getId())) + .filter(contextualEntity -> libraryId.equals(contextualEntity.getId())) .findFirst() .orElseGet(() -> { - String version = this.versionProvider.getVersion(); return new ContextualEntityBuilder() - .setId(RO_CRATE_JAVA_ID) + .setId(libraryId) .addType("SoftwareApplication") .addProperty("name", "ro-crate-java") .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") - // TODO read software version and version from gradle (write into resources properties file when building and read it from there) .addProperty("version", version) .addProperty("softwareVersion", version) .addProperty("license", "Apache-2.0") diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 6a390de9..f6c85ecb 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -20,6 +20,10 @@ class RoCrateMetadataGenerationTest { + private final String currentVersionId = new ProvenanceManager().getLibraryId(); + private final String oldVersionId = new ProvenanceManager(() -> "1.0.0").getLibraryId(); + private final String newVersionId = new ProvenanceManager(() -> "2.5.3").getLibraryId(); + private final ObjectMapper objectMapper = new ObjectMapper(); private Validator validator; @@ -55,7 +59,7 @@ void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path temp JsonNode graph = rootNode.get("@graph"); // Find ro-crate-java entity - JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); assertEquals("SoftwareApplication", roCrateJavaEntity.get("@type").asText(), "ro-crate-java should be of type SoftwareApplication"); @@ -64,7 +68,7 @@ void should_ContainRoCrateJavaEntities_When_WritingEmptyCrate(@TempDir Path temp JsonNode createActionEntity = findEntityByType(graph, "CreateAction"); assertNotNull(createActionEntity, "CreateAction entity should exist"); assertNotNull(createActionEntity.get("startTime"), "CreateAction should have startTime"); - assertEquals("#ro-crate-java", createActionEntity.get("agent").get("@id").asText(), + assertEquals(this.currentVersionId, createActionEntity.get("agent").get("@id").asText(), "CreateAction should reference ro-crate-java as agent"); } @@ -86,7 +90,7 @@ void should_HaveRequiredPropertiesInRoCrateJavaEntity_When_WritingCrate(@TempDir // Parse metadata file JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); assertEquals("ro-crate-java", roCrateJavaEntity.get("name").asText(), @@ -119,7 +123,7 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P JsonNode graph = rootNode.get("@graph"); // Get both entities - JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); JsonNode createActionEntity = findEntityByType(graph, "CreateAction"); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); @@ -128,7 +132,7 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P // Test CreateAction -> ro-crate-java reference JsonNode agentRef = createActionEntity.get("agent"); assertNotNull(agentRef, "CreateAction should have agent property"); - assertEquals("#ro-crate-java", agentRef.get("@id").asText(), + assertEquals(this.currentVersionId, agentRef.get("@id").asText(), "CreateAction's agent should reference ro-crate-java"); // Test ro-crate-java -> CreateAction reference @@ -162,7 +166,7 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t JsonNode graph = rootNode.get("@graph"); // Get ro-crate-java entity - JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); // Verify actions array exists and has three entries @@ -183,14 +187,14 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t // Verify CreateAction properties assertNotNull(createAction.get("startTime"), "CreateAction should have startTime"); - assertEquals("#ro-crate-java", createAction.get("agent").get("@id").asText(), + assertEquals(this.currentVersionId, createAction.get("agent").get("@id").asText(), "CreateAction should reference ro-crate-java as agent"); // Verify UpdateAction properties for (JsonNode updateAction : updateActions) { assertNotNull(updateAction.get("startTime"), "UpdateAction should have startTime"); - assertEquals("#ro-crate-java", updateAction.get("agent").get("@id").asText(), + assertEquals(this.currentVersionId, updateAction.get("agent").get("@id").asText(), "UpdateAction should reference ro-crate-java as agent"); } @@ -222,7 +226,7 @@ void should_HaveValidVersionFormat_When_WritingCrate(@TempDir Path tempDir) thro // Parse metadata file JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); // Version format validation @SuppressWarnings("DataFlowIssue") @@ -256,7 +260,7 @@ void should_HaveCompleteMetadata_When_WritingCrate(@TempDir Path tempDir) throws // Parse metadata file JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); // Required properties with specific values assertEquals("ro-crate-java", roCrateJavaEntity.get("name").asText(), @@ -301,7 +305,7 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp JsonNode originalRoot = objectMapper.readTree(originalMetadata); JsonNode originalGraph = originalRoot.get("@graph"); - assertNull(findEntityById(originalGraph, "#ro-crate-java"), + assertNull(findEntityById(originalGraph, this.currentVersionId), "Original crate should not have ro-crate-java entity"); assertNull(findEntityByType(originalGraph, "CreateAction"), "Original crate should not have CreateAction"); @@ -326,7 +330,7 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp JsonNode modifiedGraph = modifiedRoot.get("@graph"); // Verify ro-crate-java entity was added - JsonNode roCrateJavaEntity = findEntityById(modifiedGraph, "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(modifiedGraph, this.currentVersionId); assertNotNull(roCrateJavaEntity, "ro-crate-java entity should be added"); // Should only have UpdateAction, no CreateAction @@ -339,7 +343,7 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp // Verify update action properties assertNotNull(updateAction.get("startTime"), "UpdateAction should have startTime"); - assertEquals("#ro-crate-java", + assertEquals(this.currentVersionId, updateAction.get("agent").get("@id").asText(), "UpdateAction should reference ro-crate-java as agent"); @@ -390,7 +394,7 @@ void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir "Update should be after creation"); // Verify ro-crate-java entity references both actions - JsonNode roCrateJavaEntity = findEntityById(graph, "#ro-crate-java"); + JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); //noinspection DataFlowIssue assertTrue(roCrateJavaEntity.get("Action").isArray(), "ro-crate-java should have an array of actions"); From 0fba28d377c1d49a17738e8bb59d5eba447f7ba7 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 14:54:44 +0200 Subject: [PATCH 30/47] feat: action entities in provenance now actually call the crate their result --- .../datamanager/ro_crate/writer/ProvenanceManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 2dd19d6a..ac694b8b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -70,10 +70,11 @@ void addProvenanceInformation(Crate crate) { private ContextualEntity createActionEntity(boolean isFirstWrite, String libraryId) { return new ContextualEntityBuilder() - .addType(isFirstWrite ? "CreateAction" : "UpdateAction") - .addProperty("startTime", Instant.now().toString()) - .addIdProperty("agent", libraryId) - .build(); + .addType(isFirstWrite ? "CreateAction" : "UpdateAction") + .addIdProperty("result", "./") + .addProperty("startTime", Instant.now().toString()) + .addIdProperty("agent", libraryId) + .build(); } private ContextualEntity buildRoCrateJavaEntity( From fe179bf89482735d39ea7b03037e04832bda8f6e Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 14:55:56 +0200 Subject: [PATCH 31/47] feat: add utility class for handling RO-Crate graph operations --- .../kit/datamanager/ro_crate/util/Graph.java | 56 +++++++++++++++++++ .../writer/RoCrateMetadataGenerationTest.java | 26 +-------- 2 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java new file mode 100644 index 00000000..d9eb3bf9 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -0,0 +1,56 @@ +package edu.kit.datamanager.ro_crate.util; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.stream.StreamSupport; + +/** + * Utility class for handling operations on RO-Crate graphs. + * Provides methods to find entities by ID or type within a graph. + *

+ * {@see JsonUtilFunctions}. + */ +public class Graph { + /** + * Finds an entity in the graph by its ID. + * + * @param graph The JSON node representing the graph. + * @param id The ID of the entity to find. + * @return The entity as a JsonNode if found, null otherwise. + */ + public static JsonNode findEntityById(JsonNode graph, String id) { + for (JsonNode entity : graph) { + if (entity.has("@id") && entity.get("@id").asText().equals(id)) { + return entity; + } + } + return null; + } + + /** + * Finds an entity in the graph by its type. + * + * @param graph The JSON node representing the graph. + * @param type The type of the entity to find. + * @return The entity as a JsonNode if found, null otherwise. + */ + public static JsonNode findEntityByType(JsonNode graph, String type) { + return StreamSupport.stream(graph.spliterator(), false) + .filter(entity -> entity.path("@type").asText().equals(type)) + .findFirst() + .orElse(null); + } + + /** + * Finds all entities in the graph by their type. + * + * @param graph The JSON node representing the graph. + * @param type The type of the entities to find. + * @return An array of JsonNode containing all entities of the specified type. + */ + public static JsonNode[] findEntitiesByType(JsonNode graph, String type) { + return StreamSupport.stream(graph.spliterator(), false) + .filter(entity -> entity.path("@type").asText().equals(type)) + .toArray(JsonNode[]::new); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index f6c85ecb..80e567e3 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -14,8 +14,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.StreamSupport; +import static edu.kit.datamanager.ro_crate.util.Graph.*; import static org.junit.jupiter.api.Assertions.*; class RoCrateMetadataGenerationTest { @@ -401,28 +401,4 @@ void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir assertEquals(2, roCrateJavaEntity.get("Action").size(), "should have both actions"); } - - private JsonNode findEntityById(JsonNode graph, String id) { - for (JsonNode entity : graph) { - if (entity.has("@id") && entity.get("@id").asText().equals(id)) { - return entity; - } - } - return null; - } - - private JsonNode findEntityByType(JsonNode graph, String type) { - for (JsonNode entity : graph) { - if (entity.has("@type") && entity.get("@type").asText().equals(type)) { - return entity; - } - } - return null; - } - - private JsonNode[] findEntitiesByType(JsonNode graph, String type) { - return StreamSupport.stream(graph.spliterator(), false) - .filter(entity -> entity.has("@type") && entity.get("@type").asText().equals(type)) - .toArray(JsonNode[]::new); - } } From f4046ea3d7b0111f009d1f5f6216e8d07fadcd7d Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 15:00:12 +0200 Subject: [PATCH 32/47] test: add unit tests for ProvenanceManager library ID retrieval --- .../ro_crate/writer/ProvenanceManagerTest.java | 9 +++++++++ .../ro_crate/writer/RoCrateMetadataGenerationTest.java | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java new file mode 100644 index 00000000..c684c485 --- /dev/null +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java @@ -0,0 +1,9 @@ +package edu.kit.datamanager.ro_crate.writer; + +import static org.junit.jupiter.api.Assertions.*; +class ProvenanceManagerTest { + + private final String oldVersionId = new ProvenanceManager(() -> "1.0.0").getLibraryId(); + private final String newVersionId = new ProvenanceManager(() -> "2.5.3").getLibraryId(); + +} \ No newline at end of file diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 80e567e3..1c2e798a 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -21,8 +21,6 @@ class RoCrateMetadataGenerationTest { private final String currentVersionId = new ProvenanceManager().getLibraryId(); - private final String oldVersionId = new ProvenanceManager(() -> "1.0.0").getLibraryId(); - private final String newVersionId = new ProvenanceManager(() -> "2.5.3").getLibraryId(); private final ObjectMapper objectMapper = new ObjectMapper(); private Validator validator; From 9656ea75e15b3d81019104faba166557c1d7c024 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 15:28:31 +0200 Subject: [PATCH 33/47] feat: add getIdProperty method to retrieve a nested id property value as String --- .../ro_crate/entities/AbstractEntity.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index f0c75763..d9fb1330 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -112,6 +112,21 @@ public JsonNode getProperty(String propertyKey) { return this.properties.get(propertyKey); } + /** + * Returns the value of the property with the given key as a String. + * If the property is not found, it returns null. + * + * @param propertyKey the key of the property. + * @return the value of the property as a String or null if not found. + */ + public String getIdProperty(String propertyKey) { + JsonNode node = this.properties.get(propertyKey); + if (node != null) { + return node.path("@id").asText(null); + } + return null; + } + @JsonIgnore public String getId() { JsonNode id = this.properties.get("@id"); From 6cbbbc8d89ee4760246258c18b6072ed74cc51f6 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 15:30:05 +0200 Subject: [PATCH 34/47] test: add unit tests for ProvenanceManager --- .../ro_crate/writer/ProvenanceManager.java | 8 +- .../writer/ProvenanceManagerTest.java | 179 +++++++++++++++++- 2 files changed, 180 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index ac694b8b..de863044 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -14,7 +14,7 @@ * Handles the creation and updating of ro-crate-java entity and its actions. */ class ProvenanceManager { - private record IdPrefix(String prefix) { + protected record IdPrefix(String prefix) { public String withSuffix(String suffix) { return prefix + "-" + suffix; } @@ -25,7 +25,7 @@ public String toString() { } } - private static final IdPrefix RO_CRATE_JAVA_ID = new IdPrefix("#ro-crate-java"); + protected static final IdPrefix RO_CRATE_JAVA_ID_PREFIX = new IdPrefix("#ro-crate-java"); protected VersionProvider versionProvider; @@ -46,13 +46,13 @@ public ProvenanceManager(VersionProvider versionProvider) { } public String getLibraryId() { - return RO_CRATE_JAVA_ID.withSuffix(versionProvider.getVersion().toLowerCase()); + return RO_CRATE_JAVA_ID_PREFIX.withSuffix(versionProvider.getVersion().toLowerCase()); } void addProvenanceInformation(Crate crate) { // Determine if this is the first write boolean isFirstWrite = crate.getAllContextualEntities().stream().noneMatch( - entity -> entity.getId().startsWith(RO_CRATE_JAVA_ID.toString())) + entity -> entity.getId().startsWith(RO_CRATE_JAVA_ID_PREFIX.toString())) && !crate.isImported(); String libraryId = this.getLibraryId(); diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java index c684c485..7e26355a 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java @@ -1,9 +1,182 @@ package edu.kit.datamanager.ro_crate.writer; +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.*; + class ProvenanceManagerTest { + public final String OLD_VERSION = "1.0.0"; + private final ProvenanceManager OLD_PROV_MANAGER = new ProvenanceManager(() -> OLD_VERSION); + private final String OLD_LIBRARY_ID = OLD_PROV_MANAGER.getLibraryId(); + + public final String NEW_VERSION = "2.5.3"; + private final ProvenanceManager NEW_PROV_MANAGER = new ProvenanceManager(() -> NEW_VERSION); + private final String NEW_LIBRARY_ID = NEW_PROV_MANAGER.getLibraryId(); + + @Test + void should_CreateInitialEntities_WithCorrectVersion() { + // Given + Crate crate = new RoCrate.RoCrateBuilder().build(); + + // When + OLD_PROV_MANAGER.addProvenanceInformation(crate); + + // Then + var entities = crate.getAllContextualEntities(); + assertEquals(2, entities.size(), "Should have created two entities"); + + // Find ro-crate-java entity + var roCrateJavaEntity = entities.stream() + .filter(e -> e.getId().equals(OLD_LIBRARY_ID)) + .findFirst() + .orElseThrow(); + + assertEquals(OLD_VERSION, roCrateJavaEntity.getProperty("version").asText()); + assertEquals(OLD_VERSION, roCrateJavaEntity.getProperty("softwareVersion").asText()); + + // Find CreateAction and verify it points to correct version + var createAction = entities.stream() + .filter(e -> e.getTypes().contains("CreateAction")) + .findFirst() + .orElseThrow(); + + assertEquals(OLD_LIBRARY_ID, createAction.getIdProperty("agent")); + } + + @Test + void should_CreateDifferentEntities_WhenDifferentVersionsModifyCrate() { + // Given + Crate crate = new RoCrate.RoCrateBuilder().build(); + + // When creating with old version + OLD_PROV_MANAGER.addProvenanceInformation(crate); + + // And modifying with new version + NEW_PROV_MANAGER.addProvenanceInformation(crate); + + // Then + var entities = crate.getAllContextualEntities(); + assertEquals(4, entities.size(), "Should have four entities (2 ro-crate-java + CreateAction + UpdateAction)"); + + // Verify both version entities exist + var oldVersionEntity = entities.stream() + .filter(e -> e.getId().equals(OLD_LIBRARY_ID)) + .findFirst() + .orElseThrow(); + var newVersionEntity = entities.stream() + .filter(e -> e.getId().equals(NEW_LIBRARY_ID)) + .findFirst() + .orElseThrow(); + + assertEquals(OLD_VERSION, oldVersionEntity.getProperty("version").asText()); + assertEquals(NEW_VERSION, newVersionEntity.getProperty("version").asText()); + + // Verify actions point to correct versions + var createAction = entities.stream() + .filter(e -> e.getTypes().contains("CreateAction")) + .findFirst() + .orElseThrow(); + var updateAction = entities.stream() + .filter(e -> e.getTypes().contains("UpdateAction")) + .findFirst() + .orElseThrow(); + + assertEquals(OLD_LIBRARY_ID, createAction.getIdProperty("agent"), + "CreateAction should point to old version"); + assertEquals(NEW_LIBRARY_ID, updateAction.getIdProperty("agent"), + "UpdateAction should point to new version"); + } + + @Test + void should_ReuseExistingVersionEntity_WhenSameVersionModifiesCrateMultipleTimes() { + // Given + Crate crate = new RoCrate.RoCrateBuilder().build(); + + // When modifying multiple times with same version + OLD_PROV_MANAGER.addProvenanceInformation(crate); + OLD_PROV_MANAGER.addProvenanceInformation(crate); + OLD_PROV_MANAGER.addProvenanceInformation(crate); + + // Then + var entities = crate.getAllContextualEntities(); + + // Should have one ro-crate-java entity and three actions + long roCrateJavaCount = entities.stream() + .filter(e -> e.getId().startsWith(ProvenanceManager.RO_CRATE_JAVA_ID_PREFIX.toString())) + .count(); + assertEquals(1, roCrateJavaCount, "Should have only one ro-crate-java entity"); + + var actions = entities.stream() + .filter(e -> e.getTypes().contains("CreateAction") || e.getTypes().contains("UpdateAction")) + .toList(); + assertEquals(3, actions.size(), "Should have three actions"); + + // All actions should point to the same version entity + for (ContextualEntity action : actions) { + assertEquals(OLD_LIBRARY_ID, action.getIdProperty("agent"), + "All actions should point to the same version entity"); + } + } + + @Test + void should_PreserveVersionSpecificMetadata_WhenModifying() { + // Given + Crate crate = new RoCrate.RoCrateBuilder().build(); + + // When creating with old version + new ProvenanceManager(() -> OLD_VERSION).addProvenanceInformation(crate); + + // And modifying with new version + new ProvenanceManager(() -> NEW_VERSION).addProvenanceInformation(crate); + + // And modifying again with old version + new ProvenanceManager(() -> OLD_VERSION).addProvenanceInformation(crate); + + // Then + var entities = crate.getAllContextualEntities(); + + // Should have exactly two ro-crate-java entities + var roCrateJavaEntities = entities.stream() + .filter(e -> e.getId().startsWith(ProvenanceManager.RO_CRATE_JAVA_ID_PREFIX.toString())) + .toList(); + assertEquals(2, roCrateJavaEntities.size(), "Should have exactly two ro-crate-java entities"); + + // Each entity should maintain its complete metadata + for (ContextualEntity entity : roCrateJavaEntities) { + assertNotNull(entity.getProperty("name"), "Should have name"); + assertNotNull(entity.getProperty("url"), "Should have url"); + assertNotNull(entity.getProperty("license"), "Should have license"); + assertEquals(entity.getProperty("version"), + entity.getProperty("softwareVersion"), + "version and softwareVersion should match"); + } + + // Actions should point to appropriate versions + var actions = entities.stream() + .filter(e -> e.getTypes().contains("CreateAction") || e.getTypes().contains("UpdateAction")) + .toList(); + assertEquals(3, actions.size(), "Should have three actions"); + + // First action (CreateAction) should point to old version + var createAction = actions.stream() + .filter(e -> e.getTypes().contains("CreateAction")) + .findFirst() + .orElseThrow(); + assertEquals(OLD_LIBRARY_ID, createAction.getIdProperty("agent"), + "CreateAction should point to old version"); + + // Update actions should point to respective versions + var updateActions = actions.stream() + .filter(e -> e.getTypes().contains("UpdateAction")) + .toList(); + assertEquals(2, updateActions.size(), "Should have two update actions"); - private final String oldVersionId = new ProvenanceManager(() -> "1.0.0").getLibraryId(); - private final String newVersionId = new ProvenanceManager(() -> "2.5.3").getLibraryId(); - + assertTrue(updateActions.stream() + .map(e -> e.getIdProperty("agent")) + .allMatch(id -> id.equals(OLD_LIBRARY_ID) || id.equals(NEW_LIBRARY_ID)), + "Update actions should point to either old or new version"); + } } \ No newline at end of file From 77c427953c2121843e702a2986360cc914b4c827 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 15:47:00 +0200 Subject: [PATCH 35/47] refactor: update withAutomaticProvenance method to accept ProvenanceManager instance This allows disabling provenance management with null, but also enable to implement derived or completely new implementations. --- .../ro_crate/preview/CratePreview.java | 2 +- .../ro_crate/writer/CrateWriter.java | 15 ++++++--- .../ro_crate/writer/ProvenanceManager.java | 32 +++++++++---------- .../ro_crate/reader/CommonReaderTest.java | 4 +-- .../ro_crate/reader/FolderReaderTest.java | 2 +- .../ro_crate/reader/ZipReaderTest.java | 2 +- .../ro_crate/reader/ZipStreamReaderTest.java | 2 +- .../ro_crate/writer/FolderWriterTest.java | 2 +- .../writer/RoCrateMetadataGenerationTest.java | 2 +- .../writer/RoCrateWriterSpec12Test.java | 2 +- .../ro_crate/writer/ZipStreamWriterTest.java | 6 ++-- .../ro_crate/writer/ZipWriterTest.java | 6 ++-- 12 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java index 6f76e708..e966b4af 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java @@ -41,7 +41,7 @@ default void generate(Crate crate, File targetDir) throws IOException { // (including preview) new CrateWriter<>(new WriteFolderStrategy().disablePreview()) // We assume the caller (e.g. a writer) already stored the provenance. - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, targetDir.getAbsolutePath()); this.saveAllToFolder(targetDir); try (var stream = Files.list(targetDir.toPath())) { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java index 19ba4e3e..6caaef0e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -15,14 +15,21 @@ public class CrateWriter { private final GenericWriterStrategy strategy; - protected boolean automaticProvenance = true; + protected ProvenanceManager provenanceManager = new ProvenanceManager(); public CrateWriter(GenericWriterStrategy strategy) { this.strategy = strategy; } - public CrateWriter withAutomaticProvenance(boolean automaticProvenance) { - this.automaticProvenance = automaticProvenance; + /** + * Sets the ProvenanceManager to be used for automatic provenance information + * generation when saving the crate. + * + * @param provenanceManager the ProvenanceManager to use. Using null will disable provenance management. + * @return this CrateWriter instance for method chaining. + */ + public CrateWriter withAutomaticProvenance(ProvenanceManager provenanceManager) { + this.provenanceManager = provenanceManager; return this; } @@ -35,7 +42,7 @@ public CrateWriter withAutomaticProvenance(boolean automaticPr public void save(Crate crate, DESTINATION_TYPE destination) throws IOException { Validator defaultValidation = new Validator(new JsonSchemaValidation()); defaultValidation.validate(crate); - if (automaticProvenance) { + if (this.provenanceManager != null) { new ProvenanceManager().addProvenanceInformation(crate); } this.strategy.save(crate, destination); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index de863044..02f9a1bf 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -13,7 +13,7 @@ * Manages provenance information for RO-Crates. * Handles the creation and updating of ro-crate-java entity and its actions. */ -class ProvenanceManager { +public class ProvenanceManager { protected record IdPrefix(String prefix) { public String withSuffix(String suffix) { return prefix + "-" + suffix; @@ -49,7 +49,7 @@ public String getLibraryId() { return RO_CRATE_JAVA_ID_PREFIX.withSuffix(versionProvider.getVersion().toLowerCase()); } - void addProvenanceInformation(Crate crate) { + protected void addProvenanceInformation(Crate crate) { // Determine if this is the first write boolean isFirstWrite = crate.getAllContextualEntities().stream().noneMatch( entity -> entity.getId().startsWith(RO_CRATE_JAVA_ID_PREFIX.toString())) @@ -68,7 +68,7 @@ void addProvenanceInformation(Crate crate) { crate.addContextualEntity(actionEntity); } - private ContextualEntity createActionEntity(boolean isFirstWrite, String libraryId) { + protected ContextualEntity createActionEntity(boolean isFirstWrite, String libraryId) { return new ContextualEntityBuilder() .addType(isFirstWrite ? "CreateAction" : "UpdateAction") .addIdProperty("result", "./") @@ -77,7 +77,7 @@ private ContextualEntity createActionEntity(boolean isFirstWrite, String library .build(); } - private ContextualEntity buildRoCrateJavaEntity( + protected ContextualEntity buildRoCrateJavaEntity( Crate crate, String newActionId, String libraryId @@ -86,19 +86,17 @@ private ContextualEntity buildRoCrateJavaEntity( ContextualEntity self = crate.getAllContextualEntities().stream() .filter(contextualEntity -> libraryId.equals(contextualEntity.getId())) .findFirst() - .orElseGet(() -> { - return new ContextualEntityBuilder() - .setId(libraryId) - .addType("SoftwareApplication") - .addProperty("name", "ro-crate-java") - .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") - .addProperty("version", version) - .addProperty("softwareVersion", version) - .addProperty("license", "Apache-2.0") - .addProperty("description", "A Java library for creating and manipulating RO-Crates") - .addIdProperty("Action", newActionId) - .build(); - } + .orElseGet(() -> new ContextualEntityBuilder() + .setId(libraryId) + .addType("SoftwareApplication") + .addProperty("name", "ro-crate-java") + .addProperty("url", "https://github.com/kit-data-manager/ro-crate-java") + .addProperty("version", version) + .addProperty("softwareVersion", version) + .addProperty("license", "Apache-2.0") + .addProperty("description", "A Java library for creating and manipulating RO-Crates") + .addIdProperty("Action", newActionId) + .build() ); self.addIdProperty("Action", newActionId); return self; diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java index 19bcf61c..196fe97e 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java @@ -128,10 +128,10 @@ default void TestWithFileWithLocation(@TempDir Path temp) throws IOException { // write raw crate and imported crate to two different directories CrateWriter writer = Writers.newFolderWriter(); writer - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(rawCrate, rawCrateTarget.toString()); writer - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(importedCrate, importedCrateTarget.toString()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java index 16e51ff9..eaee1b46 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java @@ -22,7 +22,7 @@ class FolderReaderTest implements CommonReaderTest @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newFolderWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toAbsolutePath().toString()); assertTrue(target.toFile().isDirectory()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java index 336b2a67..b6ac3ea4 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java @@ -15,7 +15,7 @@ class ZipReaderTest implements @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newZipPathWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toAbsolutePath().toString()); assertTrue(target.toFile().isFile()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java index c7acb975..7cf10b32 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java @@ -34,7 +34,7 @@ public void saveCrate(Crate crate, Path target) throws IOException { FileOutputStream fos = new FileOutputStream(target_file) ) { Writers.newZipStreamWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, fos); } assertTrue(target_file.isFile()); diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java index 06fb85b7..b6181801 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java @@ -16,7 +16,7 @@ class FolderWriterTest implements CommonWriterTest { @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newFolderWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toAbsolutePath().toString()); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 1c2e798a..c4ed4eb4 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -294,7 +294,7 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp // Use writer with disabled provenance (not implemented yet) Writers.newFolderWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(originalCrate, outputPath.toString()); // Verify the original crate has no provenance information diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java index 5b6b2d92..f3f57ef7 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java @@ -30,7 +30,7 @@ void writeDoesNotModifyTest(@TempDir Path tempDir) throws IOException, URISyntax Path targetDir = tempDir.resolve("spec12writeUnmodified"); Writers.newFolderWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, targetDir.toAbsolutePath().toString()); // compare directories diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java index 256fcd5b..feb87471 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java @@ -18,7 +18,7 @@ class ZipStreamWriterTest implements public void saveCrate(Crate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { Writers.newZipStreamWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, stream); } } @@ -27,7 +27,7 @@ public void saveCrate(Crate crate, Path target) throws IOException { public void saveCrateElnStyle(Crate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { new CrateWriter<>(new WriteZipStreamStrategy().usingElnStyle()) - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, stream); } } @@ -36,7 +36,7 @@ public void saveCrateElnStyle(Crate crate, Path target) throws IOException { public void saveCrateSubdirectoryStyle(RoCrate crate, Path target) throws IOException { try (FileOutputStream stream = new FileOutputStream(target.toFile())) { new CrateWriter<>(new WriteZipStreamStrategy().withRootSubdirectory()) - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, stream); } } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java index 251072fb..d1352f31 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java @@ -13,21 +13,21 @@ class ZipWriterTest implements @Override public void saveCrate(Crate crate, Path target) throws IOException { Writers.newZipPathWriter() - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toAbsolutePath().toString()); } @Override public void saveCrateElnStyle(Crate crate, Path target) throws IOException { new CrateWriter<>(new WriteZipStrategy().usingElnStyle()) - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toAbsolutePath().toString()); } @Override public void saveCrateSubdirectoryStyle(RoCrate crate, Path target) throws IOException { new CrateWriter<>(new WriteZipStrategy().withRootSubdirectory()) - .withAutomaticProvenance(false) + .withAutomaticProvenance(null) .save(crate, target.toString()); } } From 3b47eccf272bb0d6ce07109c91094520c28e22ef Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 15:47:37 +0200 Subject: [PATCH 36/47] doc: complete javadocs for CrateWriter --- .../edu/kit/datamanager/ro_crate/writer/CrateWriter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java index 6caaef0e..22f41955 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -17,6 +17,11 @@ public class CrateWriter { private final GenericWriterStrategy strategy; protected ProvenanceManager provenanceManager = new ProvenanceManager(); + /** + * Constructs a CrateWriter with a specified strategy for writing crates. + * + * @param strategy the strategy to use for writing crates. + */ public CrateWriter(GenericWriterStrategy strategy) { this.strategy = strategy; } From c42b1858e25aff5f9ed5f47e3c7211e4afec918f Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 16:50:44 +0200 Subject: [PATCH 37/47] test: add delay in RoCrateMetadataGenerationTest to ensure crate stability during read --- .../ro_crate/writer/RoCrateMetadataGenerationTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index c4ed4eb4..2320730f 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -361,6 +361,11 @@ void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(originalCrate, outputPath.toString()); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + // ignore + } // Now read and modify the crate RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); From 210d6ac8492210d2a38cfb90d4b84cbbca42839f Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 17:08:28 +0200 Subject: [PATCH 38/47] refactor: reduce possibly missing code coverage --- .../datamanager/ro_crate/entities/AbstractEntity.java | 8 +++----- .../util/ClasspathPropertiesVersionProvider.java | 9 ++------- .../java/edu/kit/datamanager/ro_crate/util/Graph.java | 5 +++++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index d9fb1330..6d0c95d9 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -120,11 +120,9 @@ public JsonNode getProperty(String propertyKey) { * @return the value of the property as a String or null if not found. */ public String getIdProperty(String propertyKey) { - JsonNode node = this.properties.get(propertyKey); - if (node != null) { - return node.path("@id").asText(null); - } - return null; + return Optional.ofNullable(this.properties.get(propertyKey)) + .map(jsonNode -> jsonNode.path("@id").asText(null)) + .orElse(null); } @JsonIgnore diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java index 88a5a837..8f06d84f 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -28,18 +28,13 @@ public String getVersion() { } URL resource = this.getClass().getResource("/version.properties"); - if (resource == null) { - throw new IllegalStateException( - "version.properties not found in classpath. This indicates a build configuration issue."); - } + assert resource != null : "version.properties not found in classpath"; try (InputStream input = resource.openStream()) { Properties properties = new Properties(); properties.load(input); String version = properties.getProperty("version"); - if (version == null || version.trim().isEmpty()) { - throw new IllegalStateException("No version property found in version.properties"); - } + assert version != null : "Version property not found in version.properties"; return version.trim(); } catch (IOException e) { throw new IllegalStateException("Failed to read version from properties file", e); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java index d9eb3bf9..45161f16 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -11,6 +11,11 @@ * {@see JsonUtilFunctions}. */ public class Graph { + + private Graph() { + // Private constructor to prevent instantiation + } + /** * Finds an entity in the graph by its ID. * From eb89c94b2f397d584f4742552cab98c0bccd19c8 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 17:14:07 +0200 Subject: [PATCH 39/47] chore: fix small linter complaints in AbstractEntity and ClasspathPropertiesVersionProvider --- .../ro_crate/entities/AbstractEntity.java | 12 +++++------- .../util/ClasspathPropertiesVersionProvider.java | 9 ++++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index 6d0c95d9..38afcf4e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -254,9 +254,7 @@ private static boolean addProperty(ObjectNode whereToAdd, String key, JsonNode v public void addIdProperty(String name, String id) { if (id == null || id.isBlank()) { return; } mergeIdIntoValue(id, this.properties.get(name)) - .ifPresent(newValue -> { - this.properties.set(name, newValue); - }); + .ifPresent(newValue -> this.properties.set(name, newValue)); this.linkedTo.add(id); this.notifyObservers(); } @@ -369,7 +367,7 @@ private static void checkFormatISO8601(String date) throws IllegalArgumentExcept /** * Adds a property with date time format. The property should match the ISO 8601 * date format. - * + *

* Same as {@link #addProperty(String, String)} but with internal check. * * @param key key of the property (e.g. datePublished) @@ -424,7 +422,7 @@ public T setId(String id) { if (IdentifierUtils.isValidUri(id)) { this.id = id; } else { - this.id = IdentifierUtils.encode(id).get(); + this.id = IdentifierUtils.encode(id).orElse(this.id); } } return self(); @@ -461,7 +459,7 @@ public T addTypes(Collection types) { /** * Adds a property with date time format. The property should match the ISO 8601 * date format. - * + *

* Same as {@link #addProperty(String, String)} but with internal check. * * @param key key of the property (e.g. datePublished) @@ -521,7 +519,7 @@ public T addProperty(String key, boolean value) { /** * ID properties are often used when referencing other entities within * the ROCrate. This method adds automatically such one. - * + *

* Instead of {@code "name": "id" } * this will add {@code "name" : {"@id": "id"} } * diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java index 8f06d84f..35785bae 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -6,8 +6,7 @@ import java.util.Properties; public class ClasspathPropertiesVersionProvider implements VersionProvider { - private static final String PROPERTIES_FILE = "ro-crate-java.properties"; - private static final String VERSION_KEY = "version"; + public static final String VERSION_PROPERTIES = "version.properties"; /** * Cached version to avoid repeated file/resource reads. @@ -27,14 +26,14 @@ public String getVersion() { return cachedVersion; } - URL resource = this.getClass().getResource("/version.properties"); - assert resource != null : "version.properties not found in classpath"; + URL resource = this.getClass().getResource("/" + VERSION_PROPERTIES); + assert resource != null : VERSION_PROPERTIES + " not found in classpath"; try (InputStream input = resource.openStream()) { Properties properties = new Properties(); properties.load(input); String version = properties.getProperty("version"); - assert version != null : "Version property not found in version.properties"; + assert version != null : "Version property not found in " + VERSION_PROPERTIES; return version.trim(); } catch (IOException e) { throw new IllegalStateException("Failed to read version from properties file", e); From 9cbe059be15e8d34a916aefc3eeb665f924a3c91 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 17:16:51 +0200 Subject: [PATCH 40/47] test: add sleep delays in RoCrateMetadataGenerationTest to ensure stability during crate updates --- .../writer/RoCrateMetadataGenerationTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 2320730f..3c88ef07 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -141,7 +141,7 @@ void should_HaveBidirectionalRelation_Between_RoCrateJavaAndItsAction(@TempDir P } @Test - void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) throws IOException { + void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) throws IOException, InterruptedException { // Create and write crate first time RoCrate crate = new RoCrate.RoCrateBuilder().build(); validateCrate(crate); @@ -149,11 +149,14 @@ void should_AccumulateActions_When_WritingMultipleTimes(@TempDir Path tempDir) t Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(crate, outputPath.toString()); validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); + Thread.sleep(10); // Write same crate two more times to simulate updates Writers.newFolderWriter().save(crate, outputPath.toString()); + Thread.sleep(10); Writers.newFolderWriter().save(crate, outputPath.toString()); validateCrate(Readers.newFolderReader().readCrate(outputPath.toString())); + Thread.sleep(10); // Read and print metadata for debugging String metadata = Files.readString(outputPath.resolve("ro-crate-metadata.json")); @@ -354,18 +357,14 @@ void should_AddProvenanceInfo_When_ModifyingExistingCrateWithoutProvenance(@Temp } @Test - void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir) throws IOException { + void should_PreserveExistingProvenance_When_ModifyingCrate(@TempDir Path tempDir) throws IOException, InterruptedException { // First create a crate with normal provenance RoCrate originalCrate = new RoCrate.RoCrateBuilder().build(); validateCrate(originalCrate); Path outputPath = tempDir.resolve("test-crate"); Writers.newFolderWriter().save(originalCrate, outputPath.toString()); - try { - Thread.sleep(10); - } catch (InterruptedException e) { - // ignore - } + Thread.sleep(10); // Now read and modify the crate RoCrate modifiedCrate = Readers.newFolderReader().readCrate(outputPath.toString()); From ec9e9da91f61af72f3d8a3b4b6b78be62dc96c79 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 17:39:10 +0200 Subject: [PATCH 41/47] doc: enhance documentation in ProvenanceManager for clarity and consistency --- .../ro_crate/writer/ProvenanceManager.java | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index 02f9a1bf..fd4a2c50 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -14,7 +14,19 @@ * Handles the creation and updating of ro-crate-java entity and its actions. */ public class ProvenanceManager { + /** + * A record to hold the prefix for the ro-crate-java ID. + * This is used to ensure that the ID is consistent across different versions of the library. + * Having a type for the prefix avoids using it by accident as a full ID. + */ protected record IdPrefix(String prefix) { + /** + * Constructs a String with the given suffix. + * + * @param suffix The suffix to append to the prefix. + * @return A String combining the prefix and suffix, separated by a hyphen. + * Like this: "$prefix-$suffix". + */ public String withSuffix(String suffix) { return prefix + "-" + suffix; } @@ -25,8 +37,16 @@ public String toString() { } } + /** + * The prefix for the ro-crate-java ID. + * This is used to identify the ro-crate-java entity in the crate. + */ protected static final IdPrefix RO_CRATE_JAVA_ID_PREFIX = new IdPrefix("#ro-crate-java"); + /** + * The VersionProvider used to retrieve the version of ro-crate-java. + * This allows for flexibility in how the version is determined, e.g., from a properties file. + */ protected VersionProvider versionProvider; /** @@ -45,11 +65,25 @@ public ProvenanceManager(VersionProvider versionProvider) { this.versionProvider = versionProvider; } + /** + * Returns the full ID for the ro-crate-java entity of this library version + * to be used for an entity describing it. + *

+ * The ID is constructed using the RO_CRATE_JAVA_ID_PREFIX and the version from the VersionProvider. + * + * @return The ID for the ro-crate-java entity. + */ public String getLibraryId() { return RO_CRATE_JAVA_ID_PREFIX.withSuffix(versionProvider.getVersion().toLowerCase()); } - protected void addProvenanceInformation(Crate crate) { + /** + * Adds provenance information to the given crate. + * This includes creating or updating the ro-crate-java entity and its associated action entity. + * + * @param crate The crate to which provenance information will be added. + */ + public void addProvenanceInformation(Crate crate) { // Determine if this is the first write boolean isFirstWrite = crate.getAllContextualEntities().stream().noneMatch( entity -> entity.getId().startsWith(RO_CRATE_JAVA_ID_PREFIX.toString())) @@ -58,7 +92,7 @@ protected void addProvenanceInformation(Crate crate) { String libraryId = this.getLibraryId(); // Create action entity first - ContextualEntity actionEntity = createActionEntity(isFirstWrite, libraryId); + ContextualEntity actionEntity = buildNewActionEntity(isFirstWrite, libraryId); // Create or update ro-crate-java entity ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionEntity.getId(), libraryId); @@ -68,7 +102,7 @@ protected void addProvenanceInformation(Crate crate) { crate.addContextualEntity(actionEntity); } - protected ContextualEntity createActionEntity(boolean isFirstWrite, String libraryId) { + protected ContextualEntity buildNewActionEntity(boolean isFirstWrite, String libraryId) { return new ContextualEntityBuilder() .addType(isFirstWrite ? "CreateAction" : "UpdateAction") .addIdProperty("result", "./") From f2f70acf2c63c9b8ffef555be56dea6926e1643b Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 18:06:28 +0200 Subject: [PATCH 42/47] refactor: update ClasspathPropertiesVersionProvider to use lazy initialization for version loading --- .../ro_crate/util/ClasspathPropertiesVersionProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java index 35785bae..812772cd 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -17,7 +17,7 @@ public class ClasspathPropertiesVersionProvider implements VersionProvider { * Constructs a ClasspathPropertiesVersionProvider that reads the version from a properties file in the classpath. */ public ClasspathPropertiesVersionProvider() { - this.cachedVersion = getVersion(); + // Lazy initialization - version loaded on first access } @Override From 57bf583d552e887fe8d0993ec4ec60380af6e939 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 5 Jun 2025 18:06:35 +0200 Subject: [PATCH 43/47] fix: use existing provenanceManager instance in CrateWriter for provenance information --- .../java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java index 22f41955..d708ddd7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -48,7 +48,7 @@ public void save(Crate crate, DESTINATION_TYPE destination) throws IOException { Validator defaultValidation = new Validator(new JsonSchemaValidation()); defaultValidation.validate(crate); if (this.provenanceManager != null) { - new ProvenanceManager().addProvenanceInformation(crate); + this.provenanceManager.addProvenanceInformation(crate); } this.strategy.save(crate, destination); } From 5f172da4ae0312da7b165f6a30d33ef7a678a1f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:45:13 +0000 Subject: [PATCH 44/47] chore(deps): update dependency gradle to v8.14.2 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 002b867c..ff23a68d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From fa6dd224f0f5fa562a147e674b62ac627d75d29f Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Fri, 6 Jun 2025 13:45:40 +0200 Subject: [PATCH 45/47] feat: add updateCff task to update version and date-released in CITATION.cff during release process --- gradle/profile-release.gradle | 3 ++ gradle/updateCff.gradle | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 gradle/updateCff.gradle diff --git a/gradle/profile-release.gradle b/gradle/profile-release.gradle index 3af61472..a77b986f 100644 --- a/gradle/profile-release.gradle +++ b/gradle/profile-release.gradle @@ -17,6 +17,9 @@ apply plugin: 'net.researchgate.release' apply plugin: 'maven-publish' apply plugin: 'signing' + +apply from: 'gradle/updateCff.gradle' + //for plugin net.researchgate.release //see https://github.com/researchgate/gradle-release diff --git a/gradle/updateCff.gradle b/gradle/updateCff.gradle new file mode 100644 index 00000000..b4412fc3 --- /dev/null +++ b/gradle/updateCff.gradle @@ -0,0 +1,70 @@ +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +/** + * This file defines the updateCff task. + * + * This task hooks into the workflow of the release task, + * which, according to the source code of the release plugin, + * seems to be defined like this: + * + * tasks = [ + * "${p}createScmAdapter" as String, + * "${p}initScmAdapter" as String, + * "${p}checkCommitNeeded" as String, + * "${p}checkUpdateNeeded" as String, + * "${p}checkoutMergeToReleaseBranch" as String, + * "${p}unSnapshotVersion" as String, + * "${p}confirmReleaseVersion" as String, + * -> we insert our task here <- + * "${p}checkSnapshotDependencies" as String, + * "${p}runBuildTasks" as String, + * "${p}preTagCommit" as String, + * "${p}createReleaseTag" as String, + * "${p}checkoutMergeFromReleaseBranch" as String, + * "${p}updateVersion" as String, + * "${p}commitNewVersion" as String + * ] + */ + +tasks.register('updateCff') { + group = 'release' + description = 'Updates the version in CITATION.cff file' + + outputs.file("CITATION.cff") + + doLast { + def version = project.version.toString() + def today = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + + def cffFile = file('CITATION.cff') + def content = cffFile.text + + // Update or insert version + if (content.contains('version:')) { + content = content.replaceAll(/(?m)^version:\s*.+$/, "version: ${version}") + } else { + content = content + "\nversion: ${version}" + } + + // Update or insert date-released + if (content.contains('date-released:')) { + content = content.replaceAll(/(?m)^date-released:\s*.+$/, "date-released: ${today}") + } else { + content = content + "\ndate-released: ${today}" + } + + cffFile.text = content + println "Updated CITATION.cff to version ${version} and date-released ${today}" + } +} + +// Make sure your custom task runs after a specific task in the release sequence +tasks.named('updateCff') { + mustRunAfter(tasks.named('confirmReleaseVersion')) +} + +// Ensure subsequent tasks in the release sequence run after your custom task +tasks.named('checkSnapshotDependencies') { + dependsOn('updateCff') +} From 2507724ef9a8f3acb6da34d4241233db16a89ccd Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Fri, 6 Jun 2025 14:21:59 +0200 Subject: [PATCH 46/47] fix: updateCff gradle task does not require release definitions and can now be called without it --- build.gradle | 2 ++ gradle/profile-release.gradle | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 64b50abd..c96156f8 100644 --- a/build.gradle +++ b/build.gradle @@ -199,3 +199,5 @@ if (System.getProperty('profile') == "release") { println 'Using release profile for building ' + project.getName() apply from: 'gradle/profile-release.gradle' } + +apply from: 'gradle/updateCff.gradle' \ No newline at end of file diff --git a/gradle/profile-release.gradle b/gradle/profile-release.gradle index a77b986f..d915dc13 100644 --- a/gradle/profile-release.gradle +++ b/gradle/profile-release.gradle @@ -18,8 +18,6 @@ apply plugin: 'net.researchgate.release' apply plugin: 'maven-publish' apply plugin: 'signing' -apply from: 'gradle/updateCff.gradle' - //for plugin net.researchgate.release //see https://github.com/researchgate/gradle-release From 88fc71033066681c8f0eb5daeec5d886db324e82 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Fri, 6 Jun 2025 14:25:19 +0200 Subject: [PATCH 47/47] chore: remove unnecessary whitespace in build.gradle and profile-release.gradle --- build.gradle | 2 +- gradle/profile-release.gradle | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c96156f8..ae540446 100644 --- a/build.gradle +++ b/build.gradle @@ -200,4 +200,4 @@ if (System.getProperty('profile') == "release") { apply from: 'gradle/profile-release.gradle' } -apply from: 'gradle/updateCff.gradle' \ No newline at end of file +apply from: 'gradle/updateCff.gradle' diff --git a/gradle/profile-release.gradle b/gradle/profile-release.gradle index d915dc13..471eedc8 100644 --- a/gradle/profile-release.gradle +++ b/gradle/profile-release.gradle @@ -18,7 +18,6 @@ apply plugin: 'net.researchgate.release' apply plugin: 'maven-publish' apply plugin: 'signing' - //for plugin net.researchgate.release //see https://github.com/researchgate/gradle-release release {