diff --git a/.gitignore b/.gitignore index dc7ed9cd..ae2f3ea1 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,8 @@ fabric.properties *.nar *.ear *.zip +# but track the test fixture crates +!core/src/test/resources/crates/**/*.zip *.tar.gz *.rar diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..a7f77a3e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,239 @@ +# Migrating to the Jackson-free core + +Starting with version 3, ro-crate-java is split into a Jackson-free core and +pluggable JSON backends. This guide explains what changes for your code when +you upgrade from the previous, Jackson-based 2.x API. + +**Summary** + +- The artifact coordinates for the Jackson 2 backend are unchanged + (`edu.kit.datamanager:ro-crate-java`), so your dependency declaration keeps + working. +- The high-level API (the `RoCrate` builder and the entity builders) is + source-compatible. Most code needs no changes at all. +- Code that used Jackson types from the library (`ObjectNode`, `JsonNode`, + `ArrayNode`, …) must switch to the neutral JSON model of the core. The + method names and semantics deliberately mirror Jackson, so this is mostly a + type rename. +- A few internal Jackson classes were removed (see + [Removed classes](#removed-classes)). + +## Modules + +| Module | Artifact | Purpose | +| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| `core` | (internal, pulled in transitively) | Crate model, reader/writer, validation API, neutral JSON model — no JSON library dependency | +| `jackson2` | `edu.kit.datamanager:ro-crate-java` (historical coordinates) | Jackson 2 backend, for existing consumers | +| `jackson3` | `edu.kit.datamanager:ro-crate-java-jackson3` | Jackson 3 (`tools.jackson`) backend | + +The core contains no JSON code itself. The backend module provides it, and the +core selects the backend at runtime with Java's `ServiceLoader`: the backend +jar carries a `META-INF/services` file that names its implementation classes, +and the core loads whatever is listed there. Exactly one backend must be on +the classpath. Depending on `edu.kit.datamanager:ro-crate-java` puts the +Jackson 2 backend there; `edu.kit.datamanager:ro-crate-java-jackson3` puts the +Jackson 3 backend there. The two core entry points that use it are `Json` +(JSON input and output) and `Validation` (schema validation). + +## Type mapping + +The Jackson types that used to leak into the public API are replaced by the +neutral JSON model from the **`core`** module, package +`edu.kit.datamanager.ro_crate.json`: + +| Before (`com.fasterxml.jackson.databind…`) | After (`edu.kit.datamanager.ro_crate.json…`) | +| ------------------------------------------ | -------------------------------------------- | +| `JsonNode` | `JsonValue` | +| `ObjectNode` | `JsonObject` | +| `ArrayNode` | `JsonArray` | +| `TextNode` | `JsonString` | +| `IntNode` / `LongNode` / `DoubleNode` | `JsonNumber` | +| `BooleanNode` | `JsonBoolean` | +| `NullNode` | `JsonNull` | +| `MissingNode` | `JsonMissing` | + +The neutral model is not a general-purpose JSON library: it implements exactly +the subset of Jackson's API the library needs, with the same method names and +semantics (`path()`, `get()`, `has()`, `asText()`, `fields()`, `size()`, +iteration, …). + +## Changing the imports + +Before (Jackson types came from `com.fasterxml.jackson.databind…`): + +```java +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +ObjectNode properties = entity.getProperties(); +JsonNode name = properties.path("name"); +``` + +After (the neutral model lives in the `core` module, +`edu.kit.datamanager.ro_crate.json`): + +```java +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; + +JsonObject properties = entity.getProperties(); +JsonValue name = properties.path("name"); +``` + +## Common changes + +### Properties + +The method names are unchanged. Only the types in the signatures are now neutral. +Scalar `addProperty` overloads (`String`, `int`, `double`, `boolean`) exist on +the builders, so most property code needs no JSON types at all. The +`addProperty(String, JsonValue)` overload replaces the old +`addProperty(String, JsonNode)`: + +```java +import edu.kit.datamanager.ro_crate.json.JsonString; +import edu.kit.datamanager.ro_crate.entities.data.FileEntity; + +// was: builder.addProperty("description", new TextNode("long description")) +builder.addProperty("description", new JsonString("long description")); +``` + +### Building JSON trees + +Before, you would have used Jackson's `ObjectMapper` or `JsonNodeFactory`. +Now use the factory methods of `edu.kit.datamanager.ro_crate.json.Json` +(**`core`** module): + +```java +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; + +JsonObject obj = Json.objectNode() + .put("name", "Result table") + .put("contentSize", 26452); +JsonArray tags = Json.arrayNode().add("science").add("survey"); + +builder.setAllIfValid(obj); // was: setAll(ObjectNode); setAll is deprecated +``` + +### Parsing JSON + +The `edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper` class was +removed. Parse with the core's `Json` entry point instead, which delegates to +the active backend: + +```java +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; + +JsonValue parsed = Json.parse(new java.io.File("ro-crate-metadata.json")); +``` + +## Converting between Jackson and the neutral model + +If your code works with its own JSON trees, for example parsed with your own +`ObjectMapper`, convert them at the boundary to the neutral model. Both +backend modules provide the same helpers on their adapter class. With the +Jackson 2 backend (`edu.kit.datamanager:ro-crate-java`, package +`edu.kit.datamanager.ro_crate.json.jackson2`): + +```java +import com.fasterxml.jackson.databind.JsonNode; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson2.Jackson2JsonAdapter; + +JsonNode jacksonNode = yourObjectMapper.readTree(json); +JsonValue neutral = Jackson2JsonAdapter.toNeutral(jacksonNode); +// and back, if needed: +JsonNode again = Jackson2JsonAdapter.toJackson(neutral); +``` + +With the Jackson 3 backend (`edu.kit.datamanager:ro-crate-java-jackson3`, +package `edu.kit.datamanager.ro_crate.json.jackson3`), the same methods exist +for Jackson 3 node types: + +```java +import tools.jackson.databind.JsonNode; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson3.Jackson3JsonAdapter; + +JsonNode jacksonNode = yourJsonMapper.readTree(json); +JsonValue neutral = Jackson3JsonAdapter.toNeutral(jacksonNode); +// and back, if needed: +JsonNode again = Jackson3JsonAdapter.toJackson(neutral); +``` + +If your application uses Jackson 3, depend on the Jackson 3 artifact; the +classpath then contains only one Jackson version. The neutral model covers +only the subset of the JSON API the library needs, so do advanced tree +manipulation on your own nodes before converting. + +## Validation + +The core defines two validation interfaces: + +- Crate-level: `edu.kit.datamanager.ro_crate.validation.ValidatorStrategy`, + implemented by the JSON-schema class of the backend module: + `edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation` in the + `jackson2` module (historical fully qualified name, now with neutral JSON + types in its signatures), `edu.kit.datamanager.ro_crate.validation.jackson3.JsonSchemaValidation` + in the `jackson3` module. +- Entity-level: `edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy`, + which the entity builders use to check that properties follow the RO-Crate + rules (for example, that they are flat). + +Like the JSON backends, the implementations are listed in `META-INF/services` +files and loaded with `ServiceLoader`. The core requires exactly one +implementation of each interface on the classpath; this is why a backend +module must be present. + +### When validation runs + +The crate-level validation runs automatically at three places, always with +the discovered backend implementation: + +- when an `RoCrate` is created from a builder, +- when a crate is read from a folder or zip file, +- when a crate is saved with `CrateWriter`. + +Validation problems are printed to the standard error stream; the operation +itself continues, because `validate()` only returns a boolean, which these +places ignore. + +### Running validation explicitly + +```java +import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; +import edu.kit.datamanager.ro_crate.validation.Validator; + +Validator validator = new Validator( + new JsonSchemaValidation("my-profile-schema.json")); +boolean valid = validator.validate(crate); +``` + +With the `jackson2` backend, import +`edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation`; with the +`jackson3` backend, `edu.kit.datamanager.ro_crate.validation.jackson3.JsonSchemaValidation`. + +### Providing your own validator + +Implement the interface and pass it to the `Validator`: + +```java +Validator validator = new Validator(crate -> { + // your checks; return false and report problems if the crate is invalid + return true; +}); +validator.validate(crate); +``` + +## Removed classes + +Internal classes without a replacement: + +- `edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper` — use + `edu.kit.datamanager.ro_crate.json.Json.parse` / `Json.write` (see above). +- `edu.kit.datamanager.ro_crate.entities.serializers.HasPartSerializer` and + `edu.kit.datamanager.ro_crate.entities.serializers.ObjectNodeSerializer` — + serialization is handled by the backend's `JsonAdapter`. diff --git a/README.md b/README.md index 6d896722..4b3aa09b 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,79 @@ and avoiding crates which do not fully comply to the specification, at the same - [Instructions for your build manager (e.g., Gradle, Maven, etc.)](https://central.sonatype.com/artifact/edu.kit.datamanager/ro-crate-java) - [Quick-Start](#quick-start) +- [Migration guide: from the Jackson-based 2.x API to the Jackson-free core](MIGRATION.md) - [JavaDoc Documentation](https://javadoc.io/doc/edu.kit.datamanager/ro-crate-java) - [Related Publications](https://publikationen.bibliothek.kit.edu/publikationslisten/get.php?referencing=all&external_publications=kit&lang=de&format=html&style=kit-3lines-title_b-authors-other&consider_suborganizations=true&order=desc%20year&contributors=%5B%5B%5B%5D%2C%5B%22p20751.105%22%5D%5D%5D&title_contains=crate) +## Modules + +The library is split into a Jackson-free core plus two JSON backend modules: + +- **`core`** — the crate model, reader/writer and validation API, working on a + neutral JSON tree model (`edu.kit.datamanager.ro_crate.json`). It has no + dependency on any JSON library. JSON (de-)serialization and JSON-schema + validation are plugged in via the `JsonAdapter` and `ValidatorStrategy` + service-provider interfaces, discovered with Java's `ServiceLoader`: + exactly one backend must be on the classpath. +- **`jackson2`** — the Jackson 2 backend. It publishes under the historical + coordinates `edu.kit.datamanager:ro-crate-java`, so existing consumers keep + resolving the same artifact; it transitively pulls in the core. +- **`jackson3`** — the Jackson 3 backend (`tools.jackson`), published as + `edu.kit.datamanager:ro-crate-java-jackson3`. + +To use the Jackson 3 backend, depend on the Jackson 3 artifact instead of the +historical one, e.g. in Gradle: + +```groovy +implementation "edu.kit.datamanager:ro-crate-java-jackson3:" +``` + +No further configuration is needed: the backend registers itself via +`META-INF/services`. At runtime, the core's JSON and validation entry points +(the `edu.kit.datamanager.ro_crate.json.Json` and +`edu.kit.datamanager.ro_crate.validation.Validation` classes) discover it with +`ServiceLoader`, so they use whichever backend is on the classpath. If you do +not care about the JSON backend, do nothing: keep depending on +`edu.kit.datamanager:ro-crate-java` as before — the backend comes with it. + ## Build the library / documentation - Building (with tests): `./gradlew clean build` - Building (without tests): `./gradlew clean build -x test` - Building with release profile: `./gradlew -Dprofile=release clean build` - Doing a release: `./gradlew -Dprofile=release clean build release` - - Will prompt you about version number to use and next version number - - Will make a git tag which can later be used in a GitHub release - - A GitHub release will trigger the CI for publication. See also `.github/workflows/publishRelease.yml`. + - Will prompt you about version number to use and next version number + - Will make a git tag which can later be used in a GitHub release + - A GitHub release will trigger the CI for publication. See also `.github/workflows/publishRelease.yml`. - Build documentation: `./gradlew javadoc` On Windows, replace `./gradlew` with `gradlew.bat`. +## Examples + +The `examples` module contains small, self-contained example programs. Each +example exists twice: once written against the current API (run with either +backend) and once against the released 2.1.2 artifact (`examples/src/legacy`). +A parity test compares the outputs byte-for-byte, so the examples double as a +behavior-comparison suite between the released version and the current +development state. + +- Run every variant and compare: `./gradlew :examples:test` +- Run a single variant: `./gradlew :examples:runLegacyCreateMinimalCrate`, + `./gradlew :examples:runCurrent-jackson2CreateMinimalCrate`, + `./gradlew :examples:runCurrent-jackson3CreateMinimalCrate` +- Outputs are written to `examples/build/examples//.txt` +- List available example tasks: `./gradlew :examples:tasks --group examples` + +Each variant runs in its own JVM instance, since the JSON backends register via +`META-INF/services` and must not share a classpath. + ## RO-Crate Specification Compatibility -- ✅ [Version 1.1](https://www.researchobject.org/ro-crate/1.1/) ([Extracted examples as well-described unit tests/guide](src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java)) +- ✅ [Version 1.1](https://www.researchobject.org/ro-crate/1.1/) ([Extracted examples as well-described unit tests/guide](core/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java)) - 🛠️ Version 1.2-DRAFT - - ✅ Reading and writing crates with additional profiles or specifications ([examples for reading](src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java), [examples for writing](src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java)) - - ✅ Adding profiles or other specifications to a crate ([examples](src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java)) + - ✅ Reading and writing crates with additional profiles or specifications ([examples for reading](core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java), [examples for writing](core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java)) + - ✅ Adding profiles or other specifications to a crate ([examples](core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java)) ## Quick-start @@ -58,7 +109,7 @@ RoCrate myFirstCrate = STARTER_CRATE A built or imported crate can of course also be modified afterwards. Take a look at our further documentation: -- **There is a well-documented example-driven guide in [LearnByExampleTest.java](src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java) to help you get started.** -- You may also be interested in the examples we extracted from the [specification in version 1.1](https://www.researchobject.org/ro-crate/1.1/), which are available in [ExamplesOfSpecificationV1p1Test.java](src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java). -- There is a [module with all well-described guiding tests](src/test/java/edu/kit/datamanager/ro_crate/examples/) available. +- **There is a well-documented example-driven guide in [LearnByExampleTest.java](core/src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java) to help you get started.** +- You may also be interested in the examples we extracted from the [specification in version 1.1](https://www.researchobject.org/ro-crate/1.1/), which are available in [ExamplesOfSpecificationV1p1Test.java](core/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java). +- There is a [module with all well-described guiding tests](core/src/test/java/edu/kit/datamanager/ro_crate/examples/) available. - The [JavaDoc Documentation](https://javadoc.io/doc/edu.kit.datamanager/ro-crate-java) is also available online. diff --git a/build.gradle b/build.gradle index 708e8fe9..b81db06a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,203 +1,39 @@ plugins { - id 'java' - id 'jvm-test-suite' - id 'application' - id 'jacoco' - // Instructions: https://github.com/researchgate/gradle-release // Available versions: https://plugins.gradle.org/plugin/net.researchgate.release id 'net.researchgate.release' version '3.2.0' - id "maven-publish" // Publishing of JAR to Nexus instances (e.g., OSSRH) // https://github.com/gradle-nexus/publish-plugin id "io.github.gradle-nexus.publish-plugin" version "2.0.0" - id "io.freefair.maven-publish-java" version "9.8.2" } group = 'edu.kit.datamanager' -description = "A library for easy creation and modification of valid RO-Crates." println "Running gradle version: $gradle.gradleVersion" println "Building ${name} version: ${version}" println "JDK version: ${JavaVersion.current()}" -println "Profile (system property): ${System.getProperty('profile')}" - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -if (JavaVersion.current() == JavaVersion.VERSION_17) { - println "Setting encoding to UTF-8 manually" - compileJava.options.encoding = "UTF-8" - compileTestJava.options.encoding = "UTF-8" -} - -repositories { - mavenCentral() -} - -ext { - jacksonVersion = '2.22.4' -} - -dependencies { - // JUnit setup for testing - testImplementation(platform("org.junit:junit-bom:6.1.3")) - testImplementation('org.junit.jupiter:junit-jupiter') - testRuntimeOnly('org.junit.platform:junit-platform-launcher') - // JSON object mapping / (de-)serialization - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - // http client - implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.14' - // common file system operations - implementation group: 'commons-io', name: 'commons-io', version: '2.22.0' - // read from and write to zip files - implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.11.6' - // compare json documents in tests - implementation 'com.github.fslev:json-compare:8.4' - // url validator - implementation group: 'commons-validator', name: 'commons-validator', version: '1.11.0' - // logging - implementation group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.20' - // JSON-LD, Zenodo mapping - implementation group: 'com.apicatalog', name: 'titanium-json-ld', version: '1.7.0' - // metadata validation, profiles based on JSON schema - implementation group: "com.networknt", name: "json-schema-validator", version: "1.5.9" - implementation 'org.glassfish:jakarta.json:2.0.1' - //JTE for template processing - implementation('gg.jte:jte:3.2.4') - implementation("org.freemarker:freemarker:2.3.35") -} - -// enable -Xlint:deprecation -tasks.withType(JavaCompile).configureEach { - options.compilerArgs << "-Xlint:deprecation" -} - -logging.captureStandardOutput LogLevel.INFO -def signingTasks = tasks.withType(Sign) -tasks.withType(AbstractPublishToMaven).configureEach{ - mustRunAfter(signingTasks) - } - -sourceSets { - main { - java { - srcDirs = ['src/main/java'] - } - } - - test { - java { - // compileClasspath += sourceSets.main.output - //runtimeClasspath += sourceSets.main.output - srcDirs = ['src/test/java'] - } - } - - performanceTest { - java { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - srcDirs = ['src/performanceTest/java'] +// for plugin net.researchgate.release +// see https://github.com/researchgate/gradle-release +release { + //define template for tagging, e.g. v1.0.0 + tagTemplate = 'v${version}' +} + +// for plugin io.github.gradle-nexus.publish-plugin +// see https://github.com/gradle-nexus/publish-plugin +nexusPublishing { + repositories { + //select sonatype repository as publication destination + sonatype { + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) } } } -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." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.ContextualEntitiesPerformance' -} - -tasks.register('performanceLocalDataEntitiesBenchmark', JavaExec) { - description = "Run the local data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.LocalDataEntitiesPerformance' -} - -tasks.register('performanceRemoteDataEntitiesBenchmark', JavaExec) { - description = "Run the remote data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.RemoteDataEntitiesPerformance' -} - -tasks.register('performanceMixEntitiesBenchmark', JavaExec) { - description = "Run the remote data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.MixtureOfEntitiesPerformance' -} - -tasks.register('performanceDeletionEntitiesBenchmark', JavaExec) { - description = "Run the remote data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.DeletionEntitiesPerformance' -} - -tasks.register('performanceMultipleCratesBenchmark', JavaExec) { - description = "Run the remote data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.multiplecrates.MultipleCratesBenchmark' -} - -tasks.register('performanceReadWriteMultipleCratesBenchmark', JavaExec) { - description = "Run the remote data entities benchmarks." - classpath = sourceSets.performanceTest.runtimeClasspath - mainClass = 'edu.kit.datamanager.ro_crate.multiplecrates.MultipleCratesWriteAndRead' -} - -compileJava { - dependsOn generateVersionProps -} - -test { - useJUnitPlatform() - dependsOn generateVersionPropsTest - finalizedBy jacocoTestReport -} - -jacocoTestReport { - dependsOn test - reports { - csv.required = true - xml.required = true // codecov plugin may depend on xml format report - } -} - -jacoco { - toolVersion = "0.8.15" -} - -// maxParallelForks(2) -// failFast = true - -if (System.getProperty('profile') == "release") { - println 'Using release profile for building ' + project.getName() - apply from: 'gradle/profile-release.gradle' +subprojects { + apply from: rootProject.file('gradle/module-common.gradle') } apply from: 'gradle/updateCff.gradle' diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 00000000..5a0034fa --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,142 @@ +/* + * The Jackson-free core of ro-crate-java. + * + * This module must not have any dependency on a concrete JSON library: the + * neutral JSON tree model talks to a backend (jackson2 or jackson3 module) + * through the JsonAdapter and validation SPIs, discovered via ServiceLoader. + */ + +plugins { + id 'application' + id 'jacoco' +} + +description = "A library for easy creation and modification of valid RO-Crates (Jackson-free core)." + +repositories { + mavenCentral() +} + +dependencies { + // JUnit setup for testing + testImplementation(platform("org.junit:junit-bom:6.1.3")) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') + // the JSON backend is required to run the tests (provides the JsonAdapter) + testImplementation project(':jackson2') + // compare json documents in tests (test scope only; works on JSON strings) + testImplementation 'com.github.fslev:json-compare:8.4' + // http client + implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.14' + // common file system operations + implementation group: 'commons-io', name: 'commons-io', version: '2.22.0' + // read from and write to zip files + implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.11.6' + // url validator + implementation group: 'commons-validator', name: 'commons-validator', version: '1.11.0' + // logging + implementation group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.20' + // JSON-LD, Zenodo mapping + implementation group: 'com.apicatalog', name: 'titanium-json-ld', version: '1.7.0' + implementation 'org.glassfish:jakarta.json:2.0.1' + // template processing for the preview + implementation("org.freemarker:freemarker:2.3.35") + implementation('gg.jte:jte:3.2.4') +} + +jacocoTestReport { + dependsOn test + reports { + csv.required = true + xml.required = true // codecov plugin may depend on xml format report + } +} + +jacoco { + toolVersion = "0.8.15" +} + +// maxParallelForks(2) +// failFast = true + +// 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 contextual entities benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.ContextualEntitiesPerformance' +} + +tasks.register('performanceLocalDataEntitiesBenchmark', JavaExec) { + description = "Run the local data entities benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.LocalDataEntitiesPerformance' +} + +tasks.register('performanceRemoteDataEntitiesBenchmark', JavaExec) { + description = "Run the remote data entities benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.RemoteDataEntitiesPerformance' +} + +tasks.register('performanceMixEntitiesBenchmark', JavaExec) { + description = "Run the mixture of entities benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.MixtureOfEntitiesPerformance' +} + +tasks.register('performanceDeletionEntitiesBenchmark', JavaExec) { + description = "Run the deletion entities benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.singlecratebenchmarks.DeletionEntitiesPerformance' +} + +tasks.register('performanceMultipleCratesBenchmark', JavaExec) { + description = "Run the multiple crates benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.multiplecrates.MultipleCratesBenchmark' +} + +tasks.register('performanceReadWriteMultipleCratesBenchmark', JavaExec) { + description = "Run the multiple crates write and read benchmarks." + classpath = sourceSets.performanceTest.runtimeClasspath + mainClass = 'edu.kit.datamanager.ro_crate.multiplecrates.MultipleCratesWriteAndRead' +} + +compileJava { + dependsOn generateVersionProps +} + +test { + dependsOn generateVersionPropsTest + finalizedBy jacocoTestReport +} + +sourceSets { + performanceTest { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + srcDirs = ['src/performanceTest/java'] + } + } +} + +configurations { + performanceTestImplementation.extendsFrom implementation +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java b/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java similarity index 95% rename from src/main/java/edu/kit/datamanager/ro_crate/Crate.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java index 652ccdcc..9e90b5ef 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java @@ -1,21 +1,20 @@ package edu.kit.datamanager.ro_crate; -import java.io.File; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; -import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionConfig; -import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionResult; 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; import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; +import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionConfig; +import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionResult; import edu.kit.datamanager.ro_crate.preview.CratePreview; import edu.kit.datamanager.ro_crate.special.CrateVersion; +import java.io.File; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Set; /** * An interface describing an ROCrate. @@ -24,7 +23,6 @@ * @version 1 */ 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. @@ -53,7 +51,7 @@ public interface Crate { *

* NOTE: If there is no version in the crate, it does not comply with the * specification. - * + * * @return the class representation indication the version of this crate, if * available. */ @@ -67,7 +65,7 @@ public interface Crate { *

* This corresponds technically to all conformsTo values, excluding the RO crate * version / specification. - * + * * @return a collection of the profiles or specifications this crate conforms * to. */ @@ -143,7 +141,8 @@ public interface Crate { * @param parentId the ID of the parent entity. Must not be null. * @throws IllegalArgumentException if parentId is null or not found, or not a DataEntity. */ - void addDataEntity(DataEntity entity, String parentId) throws IllegalArgumentException; + void addDataEntity(DataEntity entity, String parentId) + throws IllegalArgumentException; void addContextualEntity(ContextualEntity entity); @@ -179,7 +178,9 @@ public interface Crate { * @param addInverseRelationships if true, also adds isPartOf relationships from child to parent * @return result object containing information about what was processed, as well as potential errors. */ - HierarchyRecognitionResult createDataEntityFileStructure(boolean addInverseRelationships); + HierarchyRecognitionResult createDataEntityFileStructure( + boolean addInverseRelationships + ); /** * Automatically recognizes hierarchical file structure from DataEntity IDs @@ -190,5 +191,7 @@ public interface Crate { * @param config configuration object specifying how the recognition should behave * @return result object containing information about what was processed, as well as potential errors. */ - HierarchyRecognitionResult createDataEntityFileStructure(HierarchyRecognitionConfig config); + HierarchyRecognitionResult createDataEntityFileStructure( + HierarchyRecognitionConfig config + ); } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java new file mode 100644 index 00000000..f68e86a0 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -0,0 +1,674 @@ +package edu.kit.datamanager.ro_crate; + +import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; +import edu.kit.datamanager.ro_crate.context.RoCrateMetadataContext; +import edu.kit.datamanager.ro_crate.entities.AbstractEntity; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; +import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; +import edu.kit.datamanager.ro_crate.externalproviders.dataentities.ImportFromDataCite; +import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognition; +import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionConfig; +import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionResult; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.payload.CratePayload; +import edu.kit.datamanager.ro_crate.payload.RoCratePayload; +import edu.kit.datamanager.ro_crate.preview.CratePreview; +import edu.kit.datamanager.ro_crate.preview.CustomPreview; +import edu.kit.datamanager.ro_crate.special.CrateVersion; +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; +import edu.kit.datamanager.ro_crate.validation.Validation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import java.io.File; +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * The class that represents a single ROCrate. + *

+ * To build or modify it, use a instance of {@link RoCrateBuilder}. In the case + * features of RO-Crate DRAFT specifications are needed, refer to + * {@link BuilderWithDraftFeatures} and its documentation. + * + * @author Nikola Tzotchev on 6.2.2022 г. + * @version 1 + */ +public class RoCrate implements Crate { + + protected final CratePayload roCratePayload; + protected CrateMetadataContext metadataContext; + protected CratePreview roCratePreview; + protected RootDataEntity rootDataEntity; + protected ContextualEntity jsonDescriptor; + + 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; + } + + public void setRoCratePreview(CratePreview preview) { + this.roCratePreview = preview; + } + + @Override + public void setMetadataContext(CrateMetadataContext metadataContext) { + this.metadataContext = metadataContext; + } + + @Override + public String getMetadataContextValueOf(String key) { + return this.metadataContext.getValueOf(key); + } + + @Override + public Set getMetadataContextKeys() { + return this.metadataContext.getKeys(); + } + + @Override + public Map getMetadataContextPairs() { + return this.metadataContext.getPairs(); + } + + public ContextualEntity getJsonDescriptor() { + return jsonDescriptor; + } + + @Override + public void setJsonDescriptor(ContextualEntity jsonDescriptor) { + this.jsonDescriptor = jsonDescriptor; + } + + @Override + public RootDataEntity getRootDataEntity() { + return rootDataEntity; + } + + @Override + public void setRootDataEntity(RootDataEntity rootDataEntity) { + this.rootDataEntity = rootDataEntity; + } + + /** + * Default constructor for creation of an empty crate. + */ + public RoCrate() { + this.roCratePayload = new RoCratePayload(); + this.untrackedFiles = new HashSet<>(); + this.metadataContext = new RoCrateMetadataContext(); + rootDataEntity = new RootDataEntity.RootDataEntityBuilder().build(); + jsonDescriptor = new JsonDescriptor(); + } + + /** + * A constructor for creating the crate using a Crate builder for easier + * creation. + * + * @param roCrateBuilder the builder to use. + */ + public RoCrate(RoCrateBuilder roCrateBuilder) { + this.roCratePayload = roCrateBuilder.payload; + this.metadataContext = roCrateBuilder.metadataContext; + this.roCratePreview = roCrateBuilder.preview; + this.rootDataEntity = roCrateBuilder.rootDataEntity; + this.jsonDescriptor = roCrateBuilder.descriptorBuilder.build(); + this.untrackedFiles = roCrateBuilder.untrackedFiles; + Validator defaultValidation = new Validator(Validation.crateStrategy()); + defaultValidation.validate(this); + } + + @Override + public Optional getVersion() { + JsonValue conformsTo = this.jsonDescriptor.getProperty("conformsTo"); + if (conformsTo.isArray()) { + return StreamSupport.stream(conformsTo.spliterator(), false) + .filter(JsonValue::isObject) + .map(obj -> obj.path("@id").asText()) + .map(CrateVersion::fromSpecUri) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } else if (conformsTo.isObject()) { + return CrateVersion.fromSpecUri(conformsTo.get("@id").asText()); + } else { + return Optional.empty(); + } + } + + @Override + public Collection getProfiles() { + JsonValue conformsTo = this.jsonDescriptor.getProperty("conformsTo"); + if (conformsTo.isArray()) { + return StreamSupport.stream(conformsTo.spliterator(), false) + .filter(JsonValue::isObject) + .map(obj -> obj.path("@id").asText()) + .collect(Collectors.toSet()); + } else { + return Collections.emptySet(); + } + } + + @Override + public String getJsonMetadata() { + JsonObject node = Json.objectNode(); + + node.setAll(this.metadataContext.getContextJsonEntity()); + + var graph = Json.arrayNode(); + graph.add(this.rootDataEntity.toJson()); + + graph.add(this.jsonDescriptor.toJson()); + if ( + this.roCratePayload != null && + this.roCratePayload.getEntitiesMetadata() != null + ) { + graph.addAll(this.roCratePayload.getEntitiesMetadata()); + } + node.set("@graph", graph); + return node.toString(); + } + + @Override + public DataEntity getDataEntityById(String id) { + return this.roCratePayload.getDataEntityById(id); + } + + @Override + public Optional getDataSetById(String id) { + DataEntity data = this.roCratePayload.getDataEntityById(id); + if (data instanceof DataSetEntity) { + return Optional.of((DataSetEntity) data); + } + return Optional.empty(); + } + + @Override + public Set getAllDataEntities() { + return new HashSet<>(this.roCratePayload.getAllDataEntities()); + } + + @Override + public ContextualEntity getContextualEntityById(String id) { + return this.roCratePayload.getContextualEntityById(id); + } + + @Override + public Set getAllContextualEntities() { + return new HashSet<>(this.roCratePayload.getAllContextualEntities()); + } + + @Override + public AbstractEntity getEntityById(String id) { + return this.roCratePayload.getEntityById(id); + } + + /** + * {@inheritDoc} + *

+ * Note: This will also link the DataEntity to the root node using the root + * nodes hasPart property. + * + * @param entity the DataEntity to add to this crate. + */ + @Override + public void addDataEntity(DataEntity entity) { + this.metadataContext.checkEntity(entity); + this.roCratePayload.addDataEntity(entity); + this.rootDataEntity.addToHasPart(entity.getId()); + } + + @Override + public void addDataEntity(DataEntity entity, String parentId) + throws IllegalArgumentException { + if (parentId == null) { + throw new IllegalArgumentException("Parent ID is null."); + } + + DataEntity parentEntity = parentId.equals("./") + ? this.getRootDataEntity() + : this.getDataEntityById(parentId); + + if (parentEntity == null) { + throw new IllegalArgumentException("Parent ID not found in the crate."); + } + + if (parentEntity.getTypes().contains("File")) { + throw new IllegalArgumentException("Parent entity cannot be a File."); + } + + if (!parentEntity.getTypes().contains("Dataset")) { + throw new IllegalArgumentException( + "Parent entity must be a Dataset in order to contain another DataEntity as a part." + ); + } + + this.metadataContext.checkEntity(entity); + + if (parentEntity instanceof DataSetEntity) { + ((DataSetEntity) parentEntity).addToHasPart(entity.getId()); + } else { + parentEntity.addProperty("hasPart", entity.getId()); + } + this.roCratePayload.addDataEntity(entity); + } + + @Override + public void addContextualEntity(ContextualEntity entity) { + this.metadataContext.checkEntity(entity); + this.roCratePayload.addContextualEntity(entity); + } + + @Override + public void deleteEntityById(String entityId) { + // delete the entity firstly + this.roCratePayload.removeEntityById(entityId); + // remove from the root data entity hasPart + this.rootDataEntity.removeFromHasPart(entityId); + // remove from the root entity and the file descriptor + JsonUtilFunctions.removeFieldsWith( + entityId, + this.rootDataEntity.getProperties() + ); + JsonUtilFunctions.removeFieldsWith( + entityId, + this.jsonDescriptor.getProperties() + ); + } + + @Override + public void setUntrackedFiles(Collection files) { + this.untrackedFiles = files; + } + + @Override + public void deleteValuePairFromContext(String key) { + this.metadataContext.deleteValuePairFromContext(key); + } + + @Override + public void deleteUrlFromContext(String key) { + this.metadataContext.deleteUrlFromContext(key); + } + + @Override + public void addFromCollection(Collection entities) { + this.roCratePayload.addEntities(entities); + } + + @Override + public void addItemFromDataCite(String locationUrl) { + ImportFromDataCite.addDataCiteToCrate(locationUrl, this); + } + + @Override + public Collection getUntrackedFiles() { + return this.untrackedFiles; + } + + @Override + public HierarchyRecognitionResult createDataEntityFileStructure( + boolean addInverseRelationships + ) { + HierarchyRecognitionConfig config = + new HierarchyRecognitionConfig().withSetInverseRelationships( + addInverseRelationships + ); + return this.createDataEntityFileStructure(config); + } + + @Override + public HierarchyRecognitionResult createDataEntityFileStructure( + HierarchyRecognitionConfig config + ) { + return new HierarchyRecognition(this, config).buildHierarchy(); + } + + /** + * The inner class builder for the easier creation of a ROCrate. + */ + public static class RoCrateBuilder { + + private static final String PROPERTY_DESCRIPTION = "description"; + + CratePayload payload; + CratePreview preview = new CustomPreview(); + CrateMetadataContext metadataContext; + ContextualEntity license; + RootDataEntity rootDataEntity; + Collection untrackedFiles = new HashSet<>(); + + JsonDescriptor.Builder descriptorBuilder = new JsonDescriptor.Builder(); + + /** + * The default constructor of a builder. + * + * @param name the name of the crate. + * @param description the description of the crate. + * @param datePublished the published date of the crate. + * @param licenseId the license identifier of the crate. + */ + public RoCrateBuilder( + String name, + String description, + String datePublished, + String licenseId + ) { + this.payload = new RoCratePayload(); + this.metadataContext = new RoCrateMetadataContext(); + this.rootDataEntity = new RootDataEntity.RootDataEntityBuilder() + .addProperty("name", name) + .addProperty(PROPERTY_DESCRIPTION, description) + .build(); + this.setLicense(licenseId); + this.addDatePublishedWithExceptions(datePublished); + } + + /** + * The default constructor of a builder. + * + * @param name the name of the crate. + * @param description the description of the crate. + * @param datePublished the published date of the crate. + * @param license the license entity of the crate. + */ + public RoCrateBuilder( + String name, + String description, + String datePublished, + ContextualEntity license + ) { + this.payload = new RoCratePayload(); + this.metadataContext = new RoCrateMetadataContext(); + this.rootDataEntity = new RootDataEntity.RootDataEntityBuilder() + .addProperty("name", name) + .addProperty(PROPERTY_DESCRIPTION, description) + .build(); + this.setLicense(license); + this.addDatePublishedWithExceptions(datePublished); + } + + /** + * A default constructor without any params where the root data entity + * will be plain. + */ + public RoCrateBuilder() { + this.payload = new RoCratePayload(); + this.metadataContext = new RoCrateMetadataContext(); + rootDataEntity = new RootDataEntity.RootDataEntityBuilder().build(); + } + + /** + * A constructor with a crate as template. + * + * @param crate the crate to copy. + */ + public RoCrateBuilder(RoCrate crate) { + this.payload = crate.roCratePayload; + this.preview = crate.roCratePreview; + this.metadataContext = crate.metadataContext; + this.rootDataEntity = crate.rootDataEntity; + this.untrackedFiles = crate.untrackedFiles; + this.descriptorBuilder = new JsonDescriptor.Builder(crate); + } + + public RoCrateBuilder addName(String name) { + this.rootDataEntity.addProperty("name", name); + return this; + } + + /** + * Adds an "identifier" property to the root data entity. + *

+ * This is useful e.g. to assign e.g. a DOI to this crate. + * @param identifier the identifier to add. + * @return this builder. + */ + public RoCrateBuilder addIdentifier(String identifier) { + this.rootDataEntity.addProperty("identifier", identifier.strip()); + return this; + } + + public RoCrateBuilder addDescription(String description) { + this.rootDataEntity.addProperty(PROPERTY_DESCRIPTION, description); + return this; + } + + /** + * Adds a data entity to the crate. + *

+ * Note: This will also link the DataEntity to the root node using the + * root nodes hasPart property. + * + * @param dataEntity the DataEntity to add to this crate. + * @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 void addDataEntity(DataEntity entity, String parentId) + throws IllegalArgumentException { + if (parentId == null) { + throw new IllegalArgumentException("Parent ID is null."); + } + + DataEntity parentEntity = parentId.equals("./") + ? this.rootDataEntity + : this.payload.getDataEntityById(parentId); + + if (parentEntity == null) { + throw new IllegalArgumentException("Parent ID not found in the crate."); + } + + if (parentEntity.getTypes().contains("File")) { + throw new IllegalArgumentException("Parent entity cannot be a File."); + } + + if (!parentEntity.getTypes().contains("Dataset")) { + throw new IllegalArgumentException( + "Parent entity must be a Dataset in order to contain another DataEntity as a part." + ); + } + + this.metadataContext.checkEntity(entity); + + if (parentEntity instanceof DataSetEntity) { + ((DataSetEntity) parentEntity).addToHasPart(entity.getId()); + } else { + parentEntity.addProperty("hasPart", entity.getId()); + } + this.payload.addDataEntity(entity); + } + + public RoCrateBuilder addContextualEntity( + ContextualEntity contextualEntity + ) { + this.metadataContext.checkEntity(contextualEntity); + this.payload.addContextualEntity(contextualEntity); + return this; + } + + /** + * Setting the license of the crate. + * + * @param license the license to set. + * @return this builder. + */ + public RoCrateBuilder setLicense(ContextualEntity license) { + this.license = license; + // From our tests, it seems like if we only have the ID for our license, we do + // not need to add an extra entity. + if (license.getProperties().size() > 1) { + this.addContextualEntity(license); + } + this.rootDataEntity.addIdProperty("license", license.getId()); + return this; + } + + /** + * Setting the license of the crate using only a license identifier. + * + * @param licenseId the licenses identifier. Should be a resolveable + * URI. + * @return the builder + */ + public RoCrateBuilder setLicense(String licenseId) { + ContextualEntity licenseEntity = + new ContextualEntity.ContextualEntityBuilder().setId(licenseId).build(); + this.setLicense(licenseEntity); + return this; + } + + /** + * Adds a property with date time format. The property should match the + * ISO 8601 date format. + * + * @param dateValue time string in ISO 8601 format + * @return this builder + * @throws IllegalArgumentException if format is not ISO 8601 + */ + public RoCrateBuilder addDatePublishedWithExceptions(String dateValue) + throws IllegalArgumentException { + this.rootDataEntity.addDateTimePropertyWithExceptions( + "datePublished", + dateValue + ); + return this; + } + + public RoCrateBuilder setContext(CrateMetadataContext context) { + this.metadataContext = context; + return this; + } + + public RoCrateBuilder addUrlToContext(String url) { + this.metadataContext.addToContextFromUrl(url); + return this; + } + + public RoCrateBuilder addValuePairToContext(String key, String value) { + this.metadataContext.addToContext(key, value); + return this; + } + + public RoCrateBuilder setPreview(CratePreview preview) { + this.preview = preview; + return this; + } + + public RoCrateBuilder addUntrackedFile(File file) { + this.untrackedFiles.add(file); + return this; + } + + /** + * @return a crate with the information from this builder. + */ + public RoCrate build() { + return new RoCrate(this); + } + } + + /** + * Builder for Crates, supporting features which are not in a final + * specification yet. + *

+ * NOTE: This will change the specification version of your crate. + *

+ * We only add features we expect to be in the new specification in the end. + * In case a feature will not make it into the specification, we will mark + * it as deprecated and remove it in new major versions. If a feature is + * finalized, it will be added to the stable {@link RoCrateBuilder} and + * marked as deprecated in this class. + */ + public static class BuilderWithDraftFeatures extends RoCrateBuilder { + + /** + * @see RoCrateBuilder#RoCrateBuilder() + */ + public BuilderWithDraftFeatures() { + super(); + } + + /** + * @see RoCrateBuilder#RoCrateBuilder(String, String, String, String) + */ + public BuilderWithDraftFeatures( + String name, + String description, + String datePublished, + String licenseId + ) { + super(name, description, datePublished, licenseId); + } + + /** + * @see RoCrateBuilder#RoCrateBuilder(String, String, String, + * ContextualEntity) + */ + public BuilderWithDraftFeatures( + String name, + String description, + String datePublished, + ContextualEntity licenseId + ) { + super(name, description, datePublished, licenseId); + } + + /** + * @see RoCrateBuilder#RoCrateBuilder(RoCrate) + */ + public BuilderWithDraftFeatures(RoCrate crate) { + super(crate); + this.descriptorBuilder = new JsonDescriptor.Builder(crate); + } + + /** + * Indicate this crate also conforms to the given specification, in + * addition to the version this builder adds. + *

+ * This is helpful for profiles or other specifications the crate + * conforms to. Can be called multiple times to add more specifications. + * + * @param specification a specification or profile this crate conforms + * to. + * @return the builder + */ + public BuilderWithDraftFeatures alsoConformsTo(URI specification) { + descriptorBuilder + .addConformsTo(specification) + // usage of a draft feature results in draft version numbers of the crate + .setVersion(CrateVersion.LATEST_UNSTABLE); + return this; + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java b/core/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java similarity index 82% rename from src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java index dfe6548e..658db7ca 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java @@ -1,9 +1,7 @@ package edu.kit.datamanager.ro_crate.context; -import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.kit.datamanager.ro_crate.entities.AbstractEntity; - +import edu.kit.datamanager.ro_crate.json.JsonObject; import java.util.Map; import java.util.Set; @@ -16,8 +14,7 @@ * @version 1 */ public interface CrateMetadataContext { - - ObjectNode getContextJsonEntity(); + JsonObject getContextJsonEntity(); boolean checkEntity(AbstractEntity entity); @@ -39,9 +36,9 @@ public interface CrateMetadataContext { Set getKeys(); /** - * Get an immutable map of the context. - * @return an immutable map containing the context key-value pairs - */ + * Get an immutable map of the context. + * @return an immutable map containing the context key-value pairs + */ Map getPairs(); void deleteValuePairFromContext(String key); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java b/core/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java similarity index 68% rename from src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java index d76cf86b..884a2f41 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java @@ -1,20 +1,15 @@ package edu.kit.datamanager.ro_crate.context; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.kit.datamanager.ro_crate.entities.AbstractEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; 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; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; @@ -28,9 +23,11 @@ */ public class RoCrateMetadataContext implements CrateMetadataContext { - public static final String DEFAULT_CONTEXT = "https://w3id.org/ro/crate/1.1/context"; - protected static final String DEFAULT_CONTEXT_LOCATION = "default_context/version1.1.json"; - protected static JsonNode defaultContext = null; + public static final String DEFAULT_CONTEXT = + "https://w3id.org/ro/crate/1.1/context"; + protected static final String DEFAULT_CONTEXT_LOCATION = + "default_context/version1.1.json"; + protected static JsonValue defaultContext = null; protected final Set urls = new HashSet<>(); protected final HashMap contextMap = new HashMap<>(); @@ -63,9 +60,8 @@ public RoCrateMetadataContext(Collection urls) { * * @param context the Json object of the context. */ - public RoCrateMetadataContext(JsonNode context) { - - Consumer addPairs = x -> { + public RoCrateMetadataContext(JsonValue context) { + Consumer addPairs = x -> { var iterate = x.fields(); while (iterate.hasNext()) { var next = iterate.next(); @@ -74,7 +70,7 @@ public RoCrateMetadataContext(JsonNode context) { } }; if (context.isArray()) { - for (JsonNode jsonNode : context) { + for (JsonValue jsonNode : context) { if (jsonNode.isTextual()) { this.addToContextFromUrl(jsonNode.asText()); } else if (jsonNode.isObject()) { @@ -108,14 +104,13 @@ public RoCrateMetadataContext(JsonNode context) { * } * * - * @return an ObjectNode containing the JSON-LD context representation + * @return an JsonObject containing the JSON-LD context representation */ @Override - public ObjectNode getContextJsonEntity() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ArrayNode array = objectMapper.createArrayNode(); - ObjectNode jsonNode = objectMapper.createObjectNode(); - ObjectNode finalNode = objectMapper.createObjectNode(); + public JsonObject getContextJsonEntity() { + JsonArray array = Json.arrayNode(); + JsonObject jsonNode = Json.objectNode(); + JsonObject finalNode = Json.objectNode(); for (String e : urls) { array.add(e); } @@ -150,19 +145,20 @@ public ObjectNode getContextJsonEntity() { */ @Override public boolean checkEntity(AbstractEntity entity) { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode node = entity.getProperties().deepCopy(); + JsonObject node = (JsonObject) entity.getProperties().deepCopy(); node.remove("@id"); node.remove("@type"); - Set types = objectMapper.convertValue( - entity.getProperties().path("@type"), - new TypeReference<>() {} + Set types = extractStringValues( + entity.getProperties().path("@type") ); - final Function isFail = checkMeStr -> this.contextMap.get(checkMeStr) == null - && this.contextMap.keySet().stream() - .noneMatch(key -> checkMeStr.startsWith(key + ":")); + 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) { @@ -188,7 +184,7 @@ public boolean checkEntity(AbstractEntity entity) { } // check if the fields of the entity are present in the context - for (var names = node.fieldNames(); names.hasNext();) { + for (var names = node.fieldNames(); names.hasNext(); ) { String s = names.next(); if (IdentifierUtils.isUrl(s)) { // full URLs are considered fine @@ -213,16 +209,17 @@ public boolean checkEntity(AbstractEntity entity) { public void addToContextFromUrl(String url) { this.urls.add(url); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - - JsonNode jsonNode = null; + JsonValue jsonNode = null; if (url.equals(DEFAULT_CONTEXT)) { if (defaultContext != null) { jsonNode = defaultContext; } else { - try { - jsonNode = objectMapper.readTree( - getClass().getClassLoader().getResource(DEFAULT_CONTEXT_LOCATION)); + try ( + var contextStream = getClass() + .getClassLoader() + .getResourceAsStream(DEFAULT_CONTEXT_LOCATION) + ) { + jsonNode = Json.parse(contextStream); } catch (IOException e) { e.printStackTrace(); } @@ -233,8 +230,7 @@ public void addToContextFromUrl(String url) { CloseableHttpResponse response; try (CloseableHttpClient httpclient = HttpClients.createDefault()) { response = httpclient.execute(httpGet); - jsonNode = objectMapper.readValue(response.getEntity().getContent(), - JsonNode.class); + jsonNode = Json.parse(response.getEntity().getContent()); } catch (IOException e) { System.err.printf("Cannot get context from url %s%n", url); return; @@ -243,17 +239,66 @@ public void addToContextFromUrl(String url) { defaultContext = jsonNode; } } - this.contextMap.putAll(objectMapper.convertValue(jsonNode.get("@context"), - new TypeReference<>() { - })); + this.contextMap.putAll(extractStringPairs(jsonNode.get("@context"))); + } + + /** + * Mirrors the former {@code convertValue(..., TypeReference>)}: + * collects the text values of a JSON value (a single string or an array of + * strings) into a set. + */ + private static Set extractStringValues(JsonValue value) { + Set result = new HashSet<>(); + if (value == null || value.isNull()) { + return result; + } + if (value.isArray()) { + for (JsonValue element : value) { + if (element.isTextual()) { + result.add(element.asText()); + } + } + } else if (value.isTextual()) { + result.add(value.asText()); + } + return result; } - /** - * 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 - */ + /** + * Mirrors the former {@code convertValue(..., TypeReference>)}: + * collects the string pairs of a context object. Non-object values mirror + * the former Jackson conversion failure (an IllegalArgumentException). + */ + private static Map extractStringPairs(JsonValue value) { + Map result = new HashMap<>(); + if (value == null || value.isNull()) { + return result; + } + if (!value.isObject()) { + throw new IllegalArgumentException( + "Cannot read context pairs from a non-object JSON value" + ); + } + for ( + Iterator names = ((JsonObject) value).fieldNames(); + names.hasNext(); + + ) { + String name = names.next(); + JsonValue entryValue = ((JsonObject) value).get(name); + if (entryValue.isTextual()) { + result.put(name, entryValue.asText()); + } + } + return result; + } + + /** + * 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); @@ -266,8 +311,9 @@ public void addToContext(String key, String value) { */ @Override public String getValueOf(String key) { - return Optional.ofNullable(this.contextMap.get(key)) - .orElseGet(() -> this.other.get(key)); + return Optional.ofNullable(this.contextMap.get(key)).orElseGet(() -> + this.other.get(key) + ); } /** @@ -310,5 +356,4 @@ public void deleteValuePairFromContext(String key) { public void deleteUrlFromContext(String url) { this.urls.remove(url); } - } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java new file mode 100644 index 00000000..36a40dfc --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -0,0 +1,729 @@ +package edu.kit.datamanager.ro_crate.entities; + +import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; +import edu.kit.datamanager.ro_crate.entities.validation.EntityValidation; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.payload.Observer; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; +import edu.kit.datamanager.ro_crate.validation.Validation; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Abstract Entity parent class of every singe item in the json metadata file. + * + * @author Nikola Tzotchev on 3.2.2022 г. + * @version 1 + */ +public class AbstractEntity { + + /** + * This set contains the types of an entity (ex. File, Dataset, ect.) It is + * a set because it does not make sense to have duplicates + */ + private Set types; + + /** + * Contains the whole list of properties of the entity It uses a custom + * serializer because of cases where a single array element should be + * displayed as a single value. ex: "key" : ["value"] + * <=> "key" : "value" + */ + private JsonObject properties; + + private static final EntityValidation entityValidation = new EntityValidation( + Validation.entityStrategy() + ); + + /** + * This set contains all the ids of the entities that are linked by + * this entity. This information is provided to crate payloads to make + * the removal of entities faster. + */ + private final Set linkedTo; + + private final Set observers; + + public void addObserver(Observer observer) { + this.observers.add(observer); + } + + private void notifyObservers() { + this.observers.forEach(observer -> observer.update(this.getId())); + } + + /** + * Constructor that takes a builder and instantiates all the fields from it. + * + * @param entityBuilder the entity builder passed to the constructor. + */ + public AbstractEntity(AbstractEntityBuilder entityBuilder) { + this.types = entityBuilder.types; + this.properties = entityBuilder.properties; + this.linkedTo = entityBuilder.relatedItems; + this.observers = new HashSet<>(); + if (this.properties.get("@id") == null) { + if (entityBuilder.id == null) { + this.properties.put("@id", UUID.randomUUID().toString()); + } else { + this.properties.put("@id", entityBuilder.id); + } + } + } + + public Set getLinkedTo() { + return linkedTo; + } + + /** + * Returns the types of this entity. + * + * @return a set of type strings. + */ + public Set getTypes() { + return types; + } + + /** + * Returns a Json object containing the properties of the entity. + * + * @return JsonObject representing the properties. + */ + public JsonObject getProperties() { + if (this.types != null) { + this.properties.set("@type", typesNode()); + } + return properties; + } + + /** + * The types as a JSON value, mirroring the former + * {@code MyObjectMapper.valueToTree(types)}: the shared mapper had + * {@code WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED} enabled, so a single type + * serialized to a plain string and multiple types to a string array + * (in the iteration order of the underlying set). + */ + private JsonValue typesNode() { + JsonArray typeArray = Json.arrayNode(); + for (String type : this.types) { + typeArray.add(type); + } + return this.types.size() == 1 ? typeArray.get(0) : typeArray; + } + + public JsonValue 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) { + return Optional.ofNullable(this.properties.get(propertyKey)) + .map(jsonNode -> jsonNode.path("@id").asText(null)) + .orElse(null); + } + + public String getId() { + JsonValue id = this.properties.get("@id"); + return id == null ? null : id.asText(); + } + + /** + * Set all the properties from a Json object to the Entity. The entities are + * first validated to filter any invalid entity properties. + * + * @param obj the object that contains all the json properties that should + * be added. + */ + public void setProperties(JsonValue obj) { + // validate whole entity + if (entityValidation.entityValidation(obj)) { + this.properties = ((JsonObject) obj).deepCopy(); + this.notifyObservers(); + } + } + + /** + * Serializes this entity into its JSON-LD representation. + * + *

This replaces the former Jackson bean serialization of entities + * (annotations plus custom serializers): properties are unwrapped with + * the same filtering the former {@code ObjectNodeSerializer} applied + * (empty objects, nulls and empty arrays skipped; empty objects removed + * from arrays; single-element arrays unwrapped), and data set entities + * append their {@code hasPart} references at the end, mirroring the + * former {@code HasPartSerializer}. + * + * @return the neutral JSON object representing this entity + */ + public JsonObject toJson() { + JsonObject output = Json.objectNode(); + // getProperties() intentionally refreshes the @type property first + JsonObject source = (JsonObject) JsonUtilFunctions.unwrapSingleArray( + getProperties() + ); + for (var field : source.entries()) { + String name = field.getKey(); + JsonValue value = field.getValue(); + if (value.isObject() && value.size() == 0) { + continue; + } + if (value.isNull()) { + continue; + } + if (value.isArray()) { + JsonArray array = (JsonArray) value; + // mirrors the former serializer: drop empty object elements + for (int i = array.size() - 1; i >= 0; i--) { + JsonValue element = array.get(i); + if (element.isObject() && element.size() == 0) { + array.remove(i); + } + } + if (array.isEmpty()) { + continue; + } + } + output.set(name, value); + } + appendAdditionalJsonProperties(output); + return output; + } + + /** + * Hook for subclasses that carry additional JSON properties beyond their + * stored tree; appends nothing by default. + */ + protected void appendAdditionalJsonProperties(JsonObject output) {} + + protected void setId(String id) { + this.properties.put("@id", id); + } + + /** + * removes one property from an entity. + * + * @param key the key of the entity, which will be removed. + */ + public void removeProperty(String key) { + this.getProperties().remove(key); + this.notifyObservers(); + } + + /** + * Removes a collection of properties from an entity. + * + * @param keys collection of keys, which will be removed. + */ + public void removeProperties(Collection keys) { + this.getProperties().remove(keys); + this.notifyObservers(); + } + + /** + * Adds a property to the entity. + *

+ * It may override values, if the key already exists. + * + * @param key the key of the property. + * @param value value of the property. + */ + public void addProperty(String key, String value) { + if (key != null && value != null) { + this.properties.put(key, value); + this.notifyObservers(); + } + } + + /** + * Adds a property to the entity. + *

+ * It may override values, if the key already exists. + * + * @param key the key of the property. + * @param value value of the property. + */ + public void addProperty(String key, long value) { + if (key != null) { + this.properties.put(key, value); + this.notifyObservers(); + } + } + + /** + * Adds a property to the entity. + *

+ * It may override values, if the key already exists. + * + * @param key the key of the property. + * @param value value of the property. + */ + public void addProperty(String key, double value) { + if (key != null) { + this.properties.put(key, value); + this.notifyObservers(); + } + } + + /** + * Adds a generic property to the entity. + *

+ * It may fail with an error message on stdout, in which case the + * value is not added. + * It may override values, if the key already exists. + * This is the most generic way to add a property. The value is a + * JsonValue that could contain anything possible. It is limited to + * objects allowed to flattened documents, which means any literal, + * an array of literals, or an object with an @id property. + * + * @param key String key of the property. + * @param value The JsonValue representing the value. + */ + public void addProperty(String key, JsonValue value) { + if (addProperty(this.properties, key, value)) { + notifyObservers(); + } + } + + private static boolean addProperty( + JsonObject whereToAdd, + String key, + JsonValue value + ) { + boolean validInput = key != null && value != null; + if (validInput && entityValidation.fieldValidation(value)) { + whereToAdd.set(key, value); + return true; + } + return false; + } + + /** + * Add a property that looks like this: "name" : {"@id" : "id"} If the + * name property already exists add a second @id to it. + * + * @param name the "key" of the property. + * @param id the "id" of the property. + */ + 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) + ); + this.linkedTo.add(id); + this.notifyObservers(); + } + + /** + * Merges the given id into the current value, + * using this representation: {"@id" : "id"}. + *

+ * The current value can be null without errors. + * Only the id will be considered in this case. + *

+ * If the id is null-ish, it will not be added, similar to a null-ish value. + * If the id is already present, nothing will be done. + * If it is not an array and the id is not present, an array will be applied. + * + * @param id the id to add. + * @param currentValue the current value of the property. + * @return The updated value of the property. + * Empty if value does not change! + */ + protected static Optional mergeIdIntoValue( + String id, + JsonValue currentValue + ) { + if (id == null || id.isBlank()) { + return Optional.empty(); + } + + JsonObject newIdObject = Json.objectNode().put("@id", id); + if ( + currentValue == null || currentValue.isNull() || currentValue.isMissing() + ) { + return Optional.ofNullable(newIdObject); + } + + boolean isIdAlready = currentValue.asText().equals(id); + boolean isIdObjectAlready = currentValue.path("@id").asText().equals(id); + boolean isArrayWithIdPresent = currentValue + .valueStream() + .anyMatch(node -> node.path("@id").asText().equals(id)); + if (isIdAlready || isIdObjectAlready || isArrayWithIdPresent) { + return Optional.empty(); + } + + if ( + currentValue.isArray() && + currentValue instanceof JsonArray currentValueAsArray + ) { + currentValueAsArray.add(newIdObject); + return Optional.of(currentValueAsArray); + } else { + // property is not an array, so we make it an array + JsonArray newNodes = Json.arrayNode(); + newNodes.add(currentValue); + newNodes.add(newIdObject); + return Optional.of(newNodes); + } + } + + /** + * Adds everything from the properties to the property "name" as id. + * + * @param name the key of the property. + * @param properties a collection containing all the id as String. + */ + public void addIdListProperties(String name, Collection properties) { + JsonArray node = Json.arrayNode(); + if (this.properties.get(name) == null) { + node = Json.arrayNode(); + } else { + if (!this.properties.get(name).isArray()) { + node.add(this.properties.get(name)); + } + } + for (String s : properties) { + this.linkedTo.add(s); + node.add(Json.objectNode().put("@id", s)); + } + if (node.size() == 1) { + this.properties.set(name, node.get(0)); + } else { + this.properties.set(name, node); + } + notifyObservers(); + } + + /** + * Adding new type to the property (which may have multiple such ones). + * + * @param type the String representing the type. + */ + public void addType(String type) { + if (this.types == null) { + this.types = new HashSet<>(); + } + this.types.add(type); + this.properties.set("@type", typesNode()); + } + + /** + * Checks if the date matches the ISO 8601 date format. + * + * @param date the date as a string + * @throws IllegalArgumentException if format does not match + */ + private static void checkFormatISO8601(String date) + throws IllegalArgumentException { + String regex = + "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(date); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Date MUST be a string in ISO 8601 format" + ); + } + } + + /** + * 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) + * @param value time string in ISO 8601 format + * @throws IllegalArgumentException if format is not ISO 8601 + */ + public void addDateTimePropertyWithExceptions(String key, String value) + throws IllegalArgumentException { + if (value != null) { + checkFormatISO8601(value); + this.properties.put(key, value); + this.notifyObservers(); + } + } + + /** + * This a builder inner class that should allow for an easier creating of + * entities. + * + * @param The type of the child builders so that they to can use the + * methods provided here. + */ + public abstract static class AbstractEntityBuilder> { + + private Set types; + protected Set relatedItems; + private JsonObject properties; + private String id; + + protected AbstractEntityBuilder() { + this.properties = Json.objectNode(); + this.relatedItems = new HashSet<>(); + } + + protected String getId() { + return this.id; + } + + /** + * Setting the id property of the entity, if the given value is not + * null. If the id is not encoded, the encoding will be done. + *

+ * NOTE: IDs are not just names! The ID may have effects + * on parts of your crate! For example: If the entity represents a + * file which will be copied into the crate, writers must use the + * ID as filename. + * + * @param id the String representing the id. + * @return the generic builder. + */ + public T setId(String id) { + if (id != null && !id.equals(RootDataEntity.ID)) { + if (IdentifierUtils.isValidUri(id)) { + this.id = id; + } else { + this.id = IdentifierUtils.encode(id).orElse(this.id); + } + } + return self(); + } + + /** + * Adding a type to the builder types. + * + * @param type the type to add. + * @return the generic builder. + */ + public T addType(String type) { + if (this.types == null) { + this.types = new HashSet<>(); + } + if (type != null && !type.isEmpty()) { + this.types.add(type); + } + return self(); + } + + /** + * Adds multiple types in one call. + * + * @param types the types to add. + * @return the generic builder. + */ + public T addTypes(Collection types) { + if (this.types == null) { + this.types = new HashSet<>(); + } + this.types.addAll(types); + return self(); + } + + /** + * 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) + * @param value time string in ISO 8601 format + * @return this builder + * @throws IllegalArgumentException if format is not ISO 8601 + */ + public T addDateTimePropertyWithExceptions(String key, String value) + throws IllegalArgumentException { + if (value != null) { + checkFormatISO8601(value); + this.properties.put(key, value); + } + return self(); + } + + /** + * Adding a property to the builder. + * + * @param key the key of the property in a string. + * @param value the JsonValue value of te property. + * @return the generic builder. + */ + public T addProperty(String key, JsonValue value) { + if (AbstractEntity.addProperty(this.properties, key, value)) { + this.relatedItems.addAll( + JsonUtilFunctions.getIdPropertiesFromProperty(value) + ); + } + return self(); + } + + /** + * Adding a property to the builder. + * + * @param key the key of the property as a string. + * @param value the value of the property as a string. + * @return the generic builder. + */ + public T addProperty(String key, String value) { + this.properties.put(key, value); + return self(); + } + + public T addProperty(String key, int value) { + this.properties.put(key, value); + return self(); + } + + public T addProperty(String key, double value) { + this.properties.put(key, value); + return self(); + } + + public T addProperty(String key, boolean value) { + this.properties.put(key, value); + return self(); + } + + /** + * 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"} } + * + * Does nothing if name or id are null. + * + * @param name the name of the ID property. + * @param id the ID. + * @return the generic builder + */ + public T addIdProperty(String name, String id) { + AbstractEntity.mergeIdIntoValue(id, this.properties.get(name)).ifPresent( + newValue -> { + this.properties.set(name, newValue); + this.relatedItems.add(id); + } + ); + return self(); + } + + /** + * This is another way of adding the ID property, this time the whole + * other Entity is provided. + * + * @param name the name of the property. + * @param entity the other entity that is referenced. + * @return the generic builder. + */ + public T addIdProperty(String name, AbstractEntity entity) { + if (entity != null) { + return addIdProperty(name, entity.getId()); + } + return self(); + } + + /** + * This adds multiple id entities to a single key. + * + * @param name the name of the property. + * @param entities the Collection containing the multiple entities. + * @return the generic builder. + */ + public T addIdFromCollectionOfEntities( + String name, + Collection entities + ) { + if (entities != null) { + for (var e : entities) { + addIdProperty(name, e); + } + } + return self(); + } + + /** + * Deprecated. Equivalent to {@link #setAllIfValid(JsonObject)}. + * + * @param properties the Json representing all the properties. + * @return the generic builder, either including all given properties + * * or unchanged. + * + * @deprecated To enforce the user know what this method does, + * we want the user to use one of the more explicitly named + * methods {@link #setAllIfValid(JsonObject)} or + * {@link #setAllIfValid(JsonObject)}. + * @see #setAllIfValid(JsonObject) + */ + @Deprecated(since = "2.1.0", forRemoval = true) + public T setAll(JsonObject properties) { + return setAllIfValid(properties); + } + + /** + * This sets everything from a json object to the property, + * if the result is valid. Otherwise, it will do nothing. + *

+ * Valid means here that the json object needs to be flat as specified + * in the RO-Crate specification. In principle, this means that + * primitives and objects referencing an ID are allowed, + * as well as arrays of these. + * + * @param properties the Json representing all the properties. + * @return the generic builder, either including all given properties + * or unchanged. + */ + public T setAllIfValid(JsonObject properties) { + if (AbstractEntity.entityValidation.entityValidation(properties)) { + this.properties = properties; + this.relatedItems.addAll( + JsonUtilFunctions.getIdPropertiesFromJsonNode(properties) + ); + } + return self(); + } + + /** + * This sets everything from a json object to the property. Can be + * useful when the entity is already available somewhere. + *

+ * Errors on validation are printed, but everything will be added. + * For more about validation, see {@link #setAllIfValid(JsonObject)}. + * + * @param properties the Json representing all the properties. + * @return the generic builder with all properties added. + */ + public T setAllUnsafe(JsonObject properties) { + // This will currently only print errors. + AbstractEntity.entityValidation.entityValidation(properties); + this.properties = properties; + JsonValue typeProps = properties.path("@type"); + if (typeProps.isArray()) { + typeProps.valueStream().forEach(value -> this.addType(value.asText())); + } else if (typeProps.isTextual()) { + this.addType(typeProps.asText()); + } + this.relatedItems.addAll( + JsonUtilFunctions.getIdPropertiesFromJsonNode(properties) + ); + return self(); + } + + public abstract T self(); + + public abstract AbstractEntity build(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java new file mode 100644 index 00000000..c803b14a --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java @@ -0,0 +1,196 @@ +package edu.kit.datamanager.ro_crate.entities.contextual; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; + +/** + * This class represents a detailing provenance of entities. + * + * Relevant specification link: Provenance + * of entities + * + * @author sabrinechelbi + */ +public class ActionEntity extends ContextualEntity { + + public ActionEntity(AbstractActionEntityBuilder entityBuilder) { + super(entityBuilder); + this.addType(entityBuilder.type.getName()); + this.addProperty("name", entityBuilder.name); + this.addProperty("description", entityBuilder.description); + + this.addDateTimePropertyWithExceptions( + "startTime", + entityBuilder.startTime + ); + this.addDateTimePropertyWithExceptions("endTime", entityBuilder.endTime); + + entityBuilder.status.ifPresent(status -> + this.addIdProperty("actionStatus", status.getId()) + ); + this.addIdProperty("agent", entityBuilder.agent); + this.addIdListProperties("instrument", entityBuilder.instruments); + this.addIdListProperties("result", entityBuilder.results); + this.addIdListProperties("object", entityBuilder.objects); + } + + abstract static class AbstractActionEntityBuilder> extends AbstractContextualEntityBuilder { + + protected ActionType type; + protected String description; + protected String name; + protected String startTime; + protected String endTime; + protected String agent; + protected Optional status = Optional.empty(); + + protected Collection objects = new HashSet<>(); + protected Collection results = new HashSet<>(); + protected Collection instruments = new HashSet<>(); + + protected AbstractActionEntityBuilder(ActionType type) { + this.type = type; + } + + public T setDescription(String description) { + this.description = description; + return self(); + } + + public T setName(String name) { + this.name = name; + return self(); + } + + public T setStartTime(String startTime) { + this.startTime = startTime; + return self(); + } + + public T setEndTime(String endTime) { + this.endTime = endTime; + return self(); + } + + public T setAgent(String agent) { + this.agent = agent; + return self(); + } + + public T setStatus(ActionStatus status) { + this.status = Optional.ofNullable(status); + return self(); + } + + /** + * Same as calling {@link #addObject(String)} with each element of the + * collection. + * + * From the specification: "A curation Action MUST have at least one object + * which associates it with either the root data entity Dataset or one of its + * components." + * + * @param objects see {@link #addObject(String)} + * @return this builder + */ + public T addObjects(Collection objects) { + this.objects.addAll(objects); + return self(); + } + + /** + * Adds a object to the collection of objects of this ActionEntity. + * + * From the specification: "A curation Action MUST have at least one object + * which associates it with either the root data entity Dataset or one of its + * components." + * + * @param object the object to add to this ActionEntity. Duplicates will be + * ignored/removed. "The object upon which the action is carried + * out, whose state is kept intact or changed. Also known as the + * semantic roles patient, affected or undergoer (which change + * their state) or theme (which doesn't). E.g. John read a book." + * (Schema.org definition) + * @return this builder + */ + public T addObject(String object) { + this.objects.add(object); + return self(); + } + + /** + * Same as calling {@link #addResult(String)} with each element of the + * collection. + * + * @param results see {@link #addResult(String)} + * @return this builder + */ + public T addResults(Collection results) { + this.results.addAll(results); + return self(); + } + + /** + * Adds a result to the collection of results of this ActionEntity. + * + * @param result the result to add to this ActionEntity. Duplicates will be + * ignored/removed. "The result produced in the action. E.g. John + * wrote a book." (Schema.org definition) + * @return this builder + */ + public T addResult(String result) { + this.results.add(result); + return self(); + } + + /** + * Same as calling {@link #addInstrument(String)} with each element of the + * collection. + * + * @param instruments see {@link #addInstrument(String)} + * @return this builder + */ + public T addInstruments(Collection instruments) { + this.instruments.addAll(instruments); + return self(); + } + + /** + * Adds a instrument to the collection of instruments of this ActionEntity. + * + * @param instrument the instrument to add to this ActionEntity. Duplicates will + * be ignored/removed. "The object that helped the agent + * perform the action. E.g. John wrote a book with a pen." + * (Schema.org definition) + * @return this builder + */ + public T addInstrument(String instrument) { + this.instruments.add(instrument); + return self(); + } + + @Override + public abstract ActionEntity build(); + } + + public static final class ActionEntityBuilder + extends AbstractActionEntityBuilder + { + + public ActionEntityBuilder(ActionType type) { + super(type); + } + + @Override + public ActionEntityBuilder self() { + return this; + } + + @Override + public ActionEntity build() { + return new ActionEntity(this); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java new file mode 100644 index 00000000..73cb5e7e --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java @@ -0,0 +1,18 @@ +package edu.kit.datamanager.ro_crate.entities.contextual; + +public enum ActionStatus { + ACTIVE_ACTION_STATUS("http://schema.org/ActiveActionStatus"), + COMPLETED_ACTION_STATUS("http://schema.org/CompletedActionStatus"), + FAILED_ACTION_STATUS("http://schema.org/FailedActionStatus"), + POTENTIAL_ACTION_STATUS("http://schema.org/PotentialActionStatus"); + + private final String id; + + ActionStatus(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java new file mode 100644 index 00000000..45ed2aa8 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java @@ -0,0 +1,20 @@ +package edu.kit.datamanager.ro_crate.entities.contextual; + +/** + * Enumeration class representing action types. + * @author sabrinechelbi + */ +public enum ActionType { + CREATE("CreateAction"), + UPDATE("UpdateAction"); + + private final String name; + + ActionType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java similarity index 74% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java index 1897b6df..c0d699d7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java @@ -14,8 +14,7 @@ public ContextualEntity(AbstractContextualEntityBuilder entityBuilder) { super(entityBuilder); } - abstract static class AbstractContextualEntityBuilder - > extends AbstractEntityBuilder { + abstract static class AbstractContextualEntityBuilder> extends AbstractEntityBuilder { @Override public abstract ContextualEntity build(); @@ -25,8 +24,9 @@ abstract static class AbstractContextualEntityBuilder * This is the Contextual entity builder base class, * as of right now it does not contain any methods. */ - public static final class ContextualEntityBuilder extends - AbstractContextualEntityBuilder { + public static final class ContextualEntityBuilder + extends AbstractContextualEntityBuilder + { @Override public ContextualEntityBuilder self() { diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java new file mode 100644 index 00000000..fec0b4e7 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java @@ -0,0 +1,96 @@ +package edu.kit.datamanager.ro_crate.entities.contextual; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.special.CrateVersion; +import java.net.URI; +import java.util.HashSet; +import java.util.Set; + +public class JsonDescriptor extends ContextualEntity { + + protected static final String CONFORMS_TO = "conformsTo"; + public static final String ID = "ro-crate-metadata.json"; + + /** + * Returns a JsonDescriptor with the conformsTo value set to the latest stable + * version. + */ + public JsonDescriptor() { + super( + staticPropertiesPrefilledBuilder().addIdProperty( + CONFORMS_TO, + CrateVersion.LATEST_STABLE.conformsTo + ) + ); + } + + protected static ContextualEntityBuilder staticPropertiesPrefilledBuilder() { + return new ContextualEntity.ContextualEntityBuilder() + .setId(ID) + .addType("CreativeWork") + .addIdProperty("about", "./"); + } + + private JsonDescriptor(ContextualEntityBuilder builder) { + super(builder); + } + + /** + * Builder for the JsonDescriptor. + *

+ * Defaults to the latest stable crate version and no other conformsTo values. + */ + public static final class Builder { + + CrateVersion version = CrateVersion.LATEST_STABLE; + Set otherConformsToValues = new HashSet<>(); + + public Builder() { + // default + } + + public Builder(Crate crate) { + crate.getVersion().ifPresent(v -> this.version = v); + this.otherConformsToValues.addAll(crate.getProfiles()); + } + + public Builder setVersion(CrateVersion version) { + this.version = version; + return this; + } + + public Builder addConformsTo(String uri) { + this.otherConformsToValues.add(uri); + return this; + } + + public Builder addConformsTo(URI uri) { + this.otherConformsToValues.add(uri.toString()); + return this; + } + + public JsonDescriptor build() { + if (this.otherConformsToValues.isEmpty()) { + // in this case we do not need an array + ContextualEntityBuilder entityBuilder = + staticPropertiesPrefilledBuilder().addIdProperty( + CONFORMS_TO, + this.version.conformsTo + ); + return new JsonDescriptor(entityBuilder); + } else { + // use an array to collect all values into an array of simple @id objects. + JsonArray array = Json.arrayNode(); + array.add(Json.objectNode().put("@id", this.version.conformsTo)); + for (String other : this.otherConformsToValues) { + array.add(Json.objectNode().put("@id", other)); + } + ContextualEntityBuilder entityBuilder = + staticPropertiesPrefilledBuilder().addProperty(CONFORMS_TO, array); + return new JsonDescriptor(entityBuilder); + } + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java similarity index 80% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java index 71f90618..42a36e02 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java @@ -15,7 +15,9 @@ public class OrganizationEntity extends ContextualEntity { * * @param entityBuilder the builder from which to create the entity. */ - public OrganizationEntity(AbstractOrganizationEntityBuilder entityBuilder) { + public OrganizationEntity( + AbstractOrganizationEntityBuilder entityBuilder + ) { super(entityBuilder); this.addProperty("address", entityBuilder.address); this.addProperty("email", entityBuilder.email); @@ -24,9 +26,7 @@ public OrganizationEntity(AbstractOrganizationEntityBuilder entityBuilder) { this.addType(TYPE); } - abstract static class AbstractOrganizationEntityBuilder> - extends AbstractContextualEntityBuilder { + abstract static class AbstractOrganizationEntityBuilder> extends AbstractContextualEntityBuilder { String address; String email; @@ -60,8 +60,9 @@ public T setLocationId(String placeId) { /** * A builder for the organization entity. */ - public static final class OrganizationEntityBuilder extends - AbstractOrganizationEntityBuilder { + public static final class OrganizationEntityBuilder + extends AbstractOrganizationEntityBuilder + { @Override public OrganizationEntityBuilder self() { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java similarity index 89% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java index 84cb8731..e211e57b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java @@ -25,9 +25,7 @@ public PersonEntity(AbstractPersonEntityBuilder entityBuilder) { this.addType(TYPE); } - abstract static class AbstractPersonEntityBuilder - > - extends AbstractContextualEntityBuilder { + abstract static class AbstractPersonEntityBuilder> extends AbstractContextualEntityBuilder { String affiliation; String contactPoint; @@ -68,7 +66,8 @@ public T setFamilyName(String familyName) { * Person Entity builder for easier creation of Person entities. */ public static final class PersonEntityBuilder - extends AbstractPersonEntityBuilder { + extends AbstractPersonEntityBuilder + { @Override public PersonEntityBuilder self() { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java similarity index 89% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java index bf5dd3e3..fc02ccd4 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java @@ -21,8 +21,7 @@ public PlaceEntity(AbstractPlaceEntityBuilder entityBuilder) { this.addType(TYPE); } - abstract static class AbstractPlaceEntityBuilder> extends - AbstractContextualEntityBuilder { + abstract static class AbstractPlaceEntityBuilder> extends AbstractContextualEntityBuilder { String geo; @@ -44,7 +43,8 @@ public T setGeo(ContextualEntity geo) { * A builder class for the easier Place entity construction. */ public static final class PlaceEntityBuilder - extends AbstractPlaceEntityBuilder { + extends AbstractPlaceEntityBuilder + { @Override public PlaceEntityBuilder self() { diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java new file mode 100644 index 00000000..520fd982 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java @@ -0,0 +1,190 @@ +package edu.kit.datamanager.ro_crate.entities.data; + +import static edu.kit.datamanager.ro_crate.special.IdentifierUtils.isUrl; + +import edu.kit.datamanager.ro_crate.entities.AbstractEntity; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * The base class of every data entity. + * + * @author Nikola Tzotchev on 4.2.2022 г. + * @version 1 + */ +public class DataEntity extends AbstractEntity { + + private Path path; + + /** + * The constructor that takes an DataEntity builder and instantiates the + * entity. + * + * @param entityBuilder the builder passed as argument. + */ + public DataEntity(AbstractDataEntityBuilder entityBuilder) { + super(entityBuilder); + if (!entityBuilder.authors.isEmpty()) { + this.addIdListProperties("author", entityBuilder.authors); + } + this.path = entityBuilder.location; + } + + /** + * Adds an author ID to the entity. + *

+ * Calling this multiple times will add multiple author IDs. + * + * @param id the identifier of the author. + */ + @SuppressWarnings("unused") + public void addAuthorId(String id) { + this.addIdProperty("author", id); + } + + /** + * Gets the path of this entity. + *

+ * When reading a crate, this will point to the entity's file within the crate. + * When creating a new entity or crate, it might point to a file outside the crate, + * as set by {@link DataEntityBuilder#setLocation(Path)}. + * Such a file will be copied into the crate when writing. + * + * @return the path to the file this entity represents. + */ + public Path getPath() { + return path; + } + + abstract static class AbstractDataEntityBuilder> extends AbstractEntityBuilder { + + private Path location; + private List authors = new ArrayList<>(); + + /** + * Sets the location of the data entity to make a copy from when writing the crate. + *

+ * Use this if you want to copy the associated file to the crate. + * Do not use it if the file should already be considered (e.g. + * as part of a DataSetEntity). + *

+ * If the ID has not been set manually in beforehand, it will be derived + * from the path. Use {@link #setId(String)} to override it or set it in + * beforehand. Note that another call of this method will + * not override the ID as it has been set by the previous call! + * + * @param path the location of the data. May be null, in which case + * nothing happens. + * @return this builder + */ + public T setLocation(Path path) { + if (path != null) { + if (this.getId() == null) { + this.setId(path.getFileName().toString()); + } + this.location = path; + } + return self(); + } + + /** + * A variant of {@link #setLocation(Path)} which may throw exceptions. + * + * @param path the location of the data + * @return this builder + * @throws IllegalArgumentException if path is null + */ + public T setLocationWithExceptions(Path path) + throws IllegalArgumentException { + if (path == null) { + throw new IllegalArgumentException( + "The given path should not be null." + ); + } + return setLocation(path); + } + + /** + * Same as {@link #setLocation(Path)} but instead of associating this + * entity with a file, it will point to some place on the internet. + *

+ * Via the specification, this means the uri will be set as the ID. This + * call is therefore equivalent to {@link #setId(String)}. + * + * @param uri the URI, should point at the data reachable on the + * internet. + * @return this builder + */ + public T setLocation(URI uri) { + if (uri != null && this.getId() == null) { + this.setId(uri.toString()); + } + return self(); + } + + /** + * A variant of {@link #setLocation(URI)} which may throw exceptions. + * + * @param uri the given uri + * @return this builder + * @throws IllegalArgumentException if uri is null or not a valid URL + */ + public T setLocationWithExceptions(URI uri) + throws IllegalArgumentException { + if (!isUrl(uri.toString())) { + throw new IllegalArgumentException( + "This Data Entity remote ID does not resolve to a valid URL." + ); + } + return setLocation(uri); + } + + public T setLicense(String id) { + this.addIdProperty("license", id); + return self(); + } + + public T setLicense(ContextualEntity license) { + this.addIdProperty("license", license.getId()); + return self(); + } + + public T addAuthor(String id) { + this.authors.add(id); + return self(); + } + + public T setContentLocation(String id) { + this.addIdProperty("contentLocation", id); + return self(); + } + + @Override + public abstract DataEntity build(); + } + + /** + * Data Entity builder class that allows for easier data entity creation. + *

+ * If not explicitly mentioned, all methods avoid Exceptions and will + * silently ignore null-parameters, in which case nothing will happen. Use + * the available *WithExceptions-methods in case you need them. + */ + public static final class DataEntityBuilder + extends AbstractDataEntityBuilder + { + + @Override + public DataEntityBuilder self() { + return this; + } + + @Override + public DataEntity build() { + return new DataEntity(this); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java new file mode 100644 index 00000000..51f701ba --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java @@ -0,0 +1,159 @@ +package edu.kit.datamanager.ro_crate.entities.data; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A helping class for the creating of Data entities of type Dataset. + * + * @author Nikola Tzotchev on 5.2.2022 г. + * @version 1 + */ +public class DataSetEntity extends DataEntity { + + public static final String TYPE = "Dataset"; + + /** + * Points to the parts of this dataset. + *

+ * This will be serialized to and deserialized from the "hasPart" property + * and exists for convenience to represent the additional capabilities of + * a DataSetEntity over a normal DataEntity. + */ + public Set hasPart; + + /** + * Constructor for instantiating a Dataset entity from the builder. + * + * @param entityBuilder the builder passed as argument. + */ + public DataSetEntity(AbstractDataSetBuilder entityBuilder) { + super(entityBuilder); + this.hasPart = entityBuilder.hasPart + .stream() + .filter(s -> !s.isBlank()) + .collect(Collectors.toSet()); + this.addType(TYPE); + } + + public void removeFromHasPart(String str) { + this.hasPart.remove(str); + } + + public void addToHasPart(String id) { + if (id != null && !id.isEmpty()) { + this.hasPart.add(id); + } + } + + /** + * Check if the hasPart property contains a specific id. + * + * @deprecated use {@link #hasPart(String)} instead. + * + * @param id the id to check for + * @return true if the id is present, false otherwise + */ + @Deprecated(forRemoval = true) + public boolean hasInHasPart(String id) { + return this.hasPart.contains(id); + } + + /** + * Check if the hasPart property contains a specific id. + * @param id the id to check for + * @return true if the id is present, false otherwise + */ + public boolean hasPart(String id) { + return this.hasPart.contains(id); + } + + /** + * Appends the hasPart references, mirroring the former + * {@code HasPartSerializer}: a single {@code {"@id": ...}} object for one + * element, an array of such objects otherwise, in set iteration order. + */ + @Override + protected void appendAdditionalJsonProperties(JsonObject output) { + if (this.hasPart.isEmpty()) { + return; + } + JsonArray array = Json.arrayNode(); + for (String id : this.hasPart) { + array.add(Json.objectNode().put("@id", id)); + } + output.set("hasPart", this.hasPart.size() == 1 ? array.get(0) : array); + } + + abstract static class AbstractDataSetBuilder> extends AbstractDataEntityBuilder { + + Set hasPart; + + public AbstractDataSetBuilder() { + this.hasPart = new HashSet<>(); + } + + public T setHasPart(Set hasPart) { + this.hasPart = hasPart; + return self(); + } + + public T addToHasPart(DataEntity dataEntity) { + if (dataEntity != null) { + this.hasPart.add(dataEntity.getId()); + this.relatedItems.add(dataEntity.getId()); + } + return self(); + } + + public T addToHasPart(String dataEntity) { + if (dataEntity != null) { + this.hasPart.add(dataEntity); + this.relatedItems.add(dataEntity); + } + return self(); + } + + @Override + public T setAllUnsafe(JsonObject properties) { + super.setAllUnsafe(properties); + JsonValue hasPart = properties.path("hasPart"); + String txt = hasPart.asText(); + if (!txt.isBlank()) { + this.hasPart.add(txt); + } + hasPart + .valueStream() + .map(JsonValue::asText) + .filter(value -> !value.isBlank()) + .forEach(value -> this.hasPart.add(value)); + return self(); + } + + @Override + public abstract DataSetEntity build(); + } + + /** + * Builder for the helping class DataSetEntity. + */ + public static final class DataSetBuilder + extends AbstractDataSetBuilder + { + + @Override + public DataSetBuilder self() { + return this; + } + + @Override + public DataSetEntity build() { + return new DataSetEntity(this); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java similarity index 83% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java index 44a2a4e1..d5eda533 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java @@ -15,8 +15,7 @@ public FileEntity(AbstractFileEntityBuilder entityBuilder) { this.addType(TYPE); } - abstract static class AbstractFileEntityBuilder> extends - AbstractDataEntityBuilder { + abstract static class AbstractFileEntityBuilder> extends AbstractDataEntityBuilder { public T setEncodingFormat(String encodingFormat) { this.addProperty("encodingFormat", encodingFormat); @@ -30,7 +29,9 @@ public T setEncodingFormat(String encodingFormat) { /** * Builder class for the easier creation of the File entities. */ - public static final class FileEntityBuilder extends AbstractFileEntityBuilder { + public static final class FileEntityBuilder + extends AbstractFileEntityBuilder + { @Override public FileEntityBuilder self() { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java similarity index 86% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java index 01133112..cc2e75e2 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java @@ -20,7 +20,8 @@ public RootDataEntity(AbstractDataSetBuilder entityBuilder) { * Builder class for easier creation of root data entities. */ public static final class RootDataEntityBuilder - extends AbstractDataSetBuilder { + extends AbstractDataSetBuilder + { @Override public RootDataEntityBuilder self() { @@ -30,9 +31,8 @@ public RootDataEntityBuilder self() { @Override public RootDataEntity build() { // small hack not to get the command line message - this.setLocation(Paths.get("./")) - .setId("./"); + this.setLocation(Paths.get("./")).setId("./"); return new RootDataEntity(this); } } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java similarity index 77% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java index 27f86e32..1d3f9a3b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java @@ -9,7 +9,10 @@ */ public class WorkflowEntity extends FileEntity { - private static final Set TYPES = Set.of("SoftwareSourceCode", "ComputationalWorkflow"); + private static final Set TYPES = Set.of( + "SoftwareSourceCode", + "ComputationalWorkflow" + ); /** * Constructor for creating the workflow entity from a builder. @@ -23,8 +26,7 @@ private WorkflowEntity(AbstractWorkflowEntityBuilder entityBuilder) { this.addIdListProperties("output", entityBuilder.output); } - abstract static class AbstractWorkflowEntityBuilder> extends AbstractFileEntityBuilder { + abstract static class AbstractWorkflowEntityBuilder> extends AbstractFileEntityBuilder { List input = new ArrayList<>(); List output = new ArrayList<>(); @@ -46,8 +48,9 @@ public T addOutput(String output) { /** * A builder for easier creation of workflow entities. */ - public static final class WorkflowEntityBuilder extends - AbstractWorkflowEntityBuilder { + public static final class WorkflowEntityBuilder + extends AbstractWorkflowEntityBuilder + { @Override public WorkflowEntityBuilder self() { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java similarity index 72% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java index aa54c3ca..c966fb04 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java @@ -1,7 +1,6 @@ package edu.kit.datamanager.ro_crate.entities.validation; - -import com.fasterxml.jackson.databind.JsonNode; +import edu.kit.datamanager.ro_crate.json.JsonValue; /** * Class used for validation of entities before their creation. @@ -14,11 +13,11 @@ public EntityValidation(EntityValidationStrategy strategy) { this.strategy = strategy; } - public boolean entityValidation(JsonNode entity) { + public boolean entityValidation(JsonValue entity) { return strategy.validateEntity(entity); } - public boolean fieldValidation(JsonNode entity) { + public boolean fieldValidation(JsonValue entity) { return strategy.validateFieldOfEntity(entity); } } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java similarity index 53% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java index 644b49e7..fd7dce9a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java @@ -1,13 +1,12 @@ package edu.kit.datamanager.ro_crate.entities.validation; -import com.fasterxml.jackson.databind.JsonNode; +import edu.kit.datamanager.ro_crate.json.JsonValue; /** * Interface for the strategy of the entity validation. */ public interface EntityValidationStrategy { + boolean validateEntity(JsonValue entity); - boolean validateEntity(JsonNode entity); - - boolean validateFieldOfEntity(JsonNode field); + boolean validateFieldOfEntity(JsonValue field); } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java similarity index 69% rename from src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java index 6a76ec02..26b99559 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java @@ -1,10 +1,5 @@ package edu.kit.datamanager.ro_crate.externalproviders.dataentities; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.RoCrate.RoCrateBuilder; @@ -15,8 +10,10 @@ import edu.kit.datamanager.ro_crate.entities.data.DataEntity; import edu.kit.datamanager.ro_crate.externalproviders.organizationprovider.RorProvider; import edu.kit.datamanager.ro_crate.externalproviders.personprovider.OrcidProvider; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; import java.io.IOException; import java.time.LocalDateTime; import java.util.ArrayList; @@ -52,7 +49,7 @@ private ImportFromDataCite() {} /** * This will take a DataCite entry and create a crate from it. - * + * * Except of the url, all parameters are optional. If present, they will be * added to the crate. * @@ -65,12 +62,12 @@ private ImportFromDataCite() {} * @return the created crate. */ public static Crate createCrateFromDataCiteResource( - String url, - Optional name, - Optional description, - Optional datePublished, - Optional licenseId) { - + String url, + Optional name, + Optional description, + Optional datePublished, + Optional licenseId + ) { RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); name.ifPresent(builder::addName); description.ifPresent(builder::addDescription); @@ -90,8 +87,9 @@ public static Crate createCrateFromDataCiteResource( * @param crate the existing crate. */ public static void addDataCiteToCrate(String url, Crate crate) { - var entities = - getEntitiesFromDataCiteResource(Objects.requireNonNull(getJsonNodeFromDataCite(url))); + var entities = getEntitiesFromDataCiteResource( + Objects.requireNonNull(getJsonNodeFromDataCite(url)) + ); crate.addFromCollection(entities); } @@ -106,12 +104,12 @@ public static void addDataCiteToCrate(String url, Crate crate) { * @return the created crate. */ public static Crate createCrateFromDataCiteJson( - JsonNode json, - Optional name, - Optional description, - Optional datePublished, - Optional licenseId) { - + JsonValue json, + Optional name, + Optional description, + Optional datePublished, + Optional licenseId + ) { RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); name.ifPresent(builder::addName); description.ifPresent(builder::addDescription); @@ -123,20 +121,18 @@ public static Crate createCrateFromDataCiteJson( return crate; } - public static void addDataCiteToCrateFromJson(JsonNode json, Crate crate) { + public static void addDataCiteToCrateFromJson(JsonValue json, Crate crate) { var entities = getEntitiesFromDataCiteResource(json); crate.addFromCollection(entities); } - private static JsonNode getJsonNodeFromDataCite(String url) { + private static JsonValue getJsonNodeFromDataCite(String url) { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet request = new HttpGet(url); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode; + JsonObject jsonNode; try { CloseableHttpResponse response = httpClient.execute(request); - jsonNode = objectMapper.readValue(response.getEntity().getContent(), - ObjectNode.class); + jsonNode = (JsonObject) Json.parse(response.getEntity().getContent()); } catch (IOException e) { e.printStackTrace(); return null; @@ -144,48 +140,51 @@ private static JsonNode getJsonNodeFromDataCite(String url) { return jsonNode; } - private static List getEntitiesFromDataCiteResource(JsonNode jsonNode) { - + private static List getEntitiesFromDataCiteResource( + JsonValue jsonNode + ) { List entityList = new ArrayList<>(); - final ObjectMapper objectMapper = MyObjectMapper.getMapper(); - final Map relationType = Stream.of(new String[][]{ - {"IsCitedBy", "isPartOf"}, - {"Cites", "citation"}, - {"IsSupplementTo", "isPartOf"}, - {"IsSupplementedBy", "isPartOf"}, - {"IsContinuedBy", "isPartOf"}, - {"Continues", "isBasedOn"}, - {"IsDescribedBy", "hasPart"}, - {"Describes", "isPartOf"}, - {"HasMetadata", "hasPart"}, - {"IsMetadataFor", "isPartOf"}, - {"HasVersion", "hasPart"}, - {"IsVersionOf", "isPartOf"}, - {"IsNewVersionOf", "isBasedOn"}, - {"IsPreviousVersionOf", "PredecessorOf"}, - {"IsPartOf", "isPartOf"}, - {"HasPart", "hasPart"}, - {"IsPublishedIn", "isPartOf"}, - {"IsReferencedBy", "isPartOf"}, - {"References", "citation"}, - {"IsDocumentedBy", "hasPart"}, - {"Documents", "isPartOf"}, - {"IsCompiledBy", "hasPart"}, - {"Compiles", "isPartOf"}, - {"IsVariantFormOf", "isBasedOn"}, - {"IsOriginalFormOf", "sameAs"}, - {"IsIdenticalTo", "sameAs"}, - {"IsReviewedBy", "hasPart"}, - {"Reviews", "isPartOf"}, - {"IsDerivedFrom", "isBasedOn"}, - {"IsSourceOf", "isBasedOn"}, - {"IsRequiredBy", "isPartOf"}, - {"Requires", "hasPart"}, - {"IsObsoletedBy", "hasPart"}, - {"Obsoletes", "isBasedOn"} - }).collect(Collectors.collectingAndThen( + final Map relationType = Stream.of(new String[][] { + { "IsCitedBy", "isPartOf" }, + { "Cites", "citation" }, + { "IsSupplementTo", "isPartOf" }, + { "IsSupplementedBy", "isPartOf" }, + { "IsContinuedBy", "isPartOf" }, + { "Continues", "isBasedOn" }, + { "IsDescribedBy", "hasPart" }, + { "Describes", "isPartOf" }, + { "HasMetadata", "hasPart" }, + { "IsMetadataFor", "isPartOf" }, + { "HasVersion", "hasPart" }, + { "IsVersionOf", "isPartOf" }, + { "IsNewVersionOf", "isBasedOn" }, + { "IsPreviousVersionOf", "PredecessorOf" }, + { "IsPartOf", "isPartOf" }, + { "HasPart", "hasPart" }, + { "IsPublishedIn", "isPartOf" }, + { "IsReferencedBy", "isPartOf" }, + { "References", "citation" }, + { "IsDocumentedBy", "hasPart" }, + { "Documents", "isPartOf" }, + { "IsCompiledBy", "hasPart" }, + { "Compiles", "isPartOf" }, + { "IsVariantFormOf", "isBasedOn" }, + { "IsOriginalFormOf", "sameAs" }, + { "IsIdenticalTo", "sameAs" }, + { "IsReviewedBy", "hasPart" }, + { "Reviews", "isPartOf" }, + { "IsDerivedFrom", "isBasedOn" }, + { "IsSourceOf", "isBasedOn" }, + { "IsRequiredBy", "isPartOf" }, + { "Requires", "hasPart" }, + { "IsObsoletedBy", "hasPart" }, + { "Obsoletes", "isBasedOn" }, + }).collect( + Collectors.collectingAndThen( Collectors.toMap(data -> data[0], data -> data[1]), - Collections::unmodifiableMap)); + Collections::unmodifiableMap + ) + ); final Collection citation = new HashSet<>(); final Collection hasPart = new HashSet<>(); @@ -193,7 +192,6 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso final Collection IsPartOf = new HashSet<>(); final Collection sameAs = new HashSet<>(); - // the id of the main entity String id = null; var identifiers = jsonNode.get("identifiers"); @@ -207,7 +205,7 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso // main entity type String type = "CreativeWork"; - JsonNode types = jsonNode.get("types"); + JsonValue types = jsonNode.get("types"); var schemaType = types.get("schemaOrg"); if (schemaType != null) { type = schemaType.asText(); @@ -215,7 +213,7 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso // the title String title; - JsonNode titles = jsonNode.get("titles"); + JsonValue titles = jsonNode.get("titles"); if (titles == null || titles.isEmpty()) { // no title title = null; @@ -225,11 +223,11 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso // publicationYear String publicationYear; - JsonNode pubYear = jsonNode.get("publicationYear"); + JsonValue pubYear = jsonNode.get("publicationYear"); publicationYear = pubYear != null ? pubYear.asText() : null; // the creators + their affiliations - JsonNode creators = jsonNode.get("creators"); + JsonValue creators = jsonNode.get("creators"); List creatorsList = new ArrayList<>(); if (creators != null) { for (var e : creators) { @@ -240,15 +238,16 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso // the publisher OrganizationEntity publisher = null; - JsonNode publisherNode = jsonNode.get("publisher"); + JsonValue publisherNode = jsonNode.get("publisher"); if (publisherNode != null && publisherNode.isTextual()) { publisher = new OrganizationEntity.OrganizationEntityBuilder() - .addProperty("name", publisherNode.asText()).build(); + .addProperty("name", publisherNode.asText()) + .build(); entityList.add(publisher); } // contributors - JsonNode contributors = jsonNode.get("contributors"); + JsonValue contributors = jsonNode.get("contributors"); List contributorsList = new ArrayList<>(); if (contributors != null) { for (var e : contributors) { @@ -258,8 +257,8 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso } // description - ArrayNode descriptionArray = objectMapper.createArrayNode(); - JsonNode descriptionNode = jsonNode.get("descriptions"); + JsonArray descriptionArray = Json.arrayNode(); + JsonValue descriptionNode = jsonNode.get("descriptions"); if (descriptionNode != null) { for (var e : descriptionNode) { var desc = e.get("description"); @@ -271,7 +270,7 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso String dateCreated = null; String dateModified = null; String datePublished = null; - JsonNode dates = jsonNode.get("dates"); + JsonValue dates = jsonNode.get("dates"); if (dates != null) { for (var el : dates) { String dataType = el.get("dateType").asText(); @@ -298,15 +297,15 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso } // subjects which are mapped to keywords - JsonNode subjects = jsonNode.get("subjects"); - ArrayNode keyWordsArray = objectMapper.createArrayNode(); + JsonValue subjects = jsonNode.get("subjects"); + JsonArray keyWordsArray = Json.arrayNode(); if (subjects != null) { for (var sub : subjects) { keyWordsArray.add(sub.get("subject").asText()); } } - Consumer addRelatedEntity = (entity) -> { + Consumer addRelatedEntity = entity -> { var mapEntry = relationType.get(entity.getProperty("keywords").asText()); if (mapEntry != null) { switch (mapEntry) { @@ -330,32 +329,32 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso }; // relatedIdentifiers - JsonNode related = jsonNode.get("relatedIdentifiers"); + JsonValue related = jsonNode.get("relatedIdentifiers"); if (related != null) { for (var element : related) { String relatedId = element.get("relatedIdentifier").asText(); String relatedIdType = element.get("relatedIdentifierType").asText(); String relationTypeString = element.get("relationType").asText(); DataEntity relatedItem = new DataEntity.DataEntityBuilder() - .addType("CreativeWork") - .setId(relatedId) - .addProperty("description", relatedIdType) - .addProperty("keywords", relationTypeString) - .build(); + .addType("CreativeWork") + .setId(relatedId) + .addProperty("description", relatedIdType) + .addProperty("keywords", relationTypeString) + .build(); entityList.add(relatedItem); addRelatedEntity.accept(relatedItem); } } - // geolocations - JsonNode geo = jsonNode.get("geoLocations"); + JsonValue geo = jsonNode.get("geoLocations"); Collection geoSet = new HashSet<>(); if (geo != null) { for (var geoThing : geo) { // geo point - ContextualEntity geoEntity = new ContextualEntity.ContextualEntityBuilder() + ContextualEntity geoEntity = + new ContextualEntity.ContextualEntityBuilder() .addType("Place") .build(); @@ -367,10 +366,10 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso if (geoThing.get("geoLocationPoint") != null) { var point = geoThing.get("geoLocationPoint"); var geoCoordinate = new ContextualEntity.ContextualEntityBuilder() - .addType("GeoCoordinates") - .addProperty("latitude", point.get("pointLatitude")) - .addProperty("longitude", point.get("pointLongitude")) - .build(); + .addType("GeoCoordinates") + .addProperty("latitude", point.get("pointLatitude")) + .addProperty("longitude", point.get("pointLongitude")) + .build(); entityList.add(geoCoordinate); geoEntity.addIdProperty("geo", geoCoordinate.getId()); } @@ -384,9 +383,14 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso String topRightLongitude = box.get("eastBoundLongitude").asText(); String topRightLatitude = box.get("northBoundLatitude").asText(); - boxString = bottomLeftLongitude + "," + bottomLeftLatitude + " " - + topRightLongitude + "," + topRightLatitude; - + boxString = + bottomLeftLongitude + + "," + + bottomLeftLatitude + + " " + + topRightLongitude + + "," + + topRightLatitude; } if (geoThing.get("geoLocationPolygon") != null) { StringBuilder stringBuilder = new StringBuilder(); @@ -394,13 +398,18 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso var point = polygonEl.get("polygonPoint"); String longitude = point.get("pointLongitude").asText(); String latitude = point.get("pointLatitude").asText(); - stringBuilder.append(longitude).append(",").append(latitude).append(" "); + stringBuilder + .append(longitude) + .append(",") + .append(latitude) + .append(" "); } polygonString = stringBuilder.toString(); } if (boxString != null || polygonString != null) { - ContextualEntity shapeEntity = new ContextualEntity.ContextualEntityBuilder() + ContextualEntity shapeEntity = + new ContextualEntity.ContextualEntityBuilder() .addType("GeoShape") .addProperty("box", boxString) .addProperty("box", polygonString) @@ -420,7 +429,7 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso } // alternateIdentifier - ArrayNode alternate = objectMapper.createArrayNode(); + JsonArray alternate = Json.arrayNode(); if (jsonNode.get("alternateIdentifiers") != null) { for (var el : jsonNode) { alternate.add(el.get("alternateIdentifier").asText()); @@ -428,15 +437,15 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso } // size - ArrayNode sizes = null; + JsonArray sizes = null; if (jsonNode.get("sizes") != null) { - sizes = (ArrayNode) jsonNode.get("sizes"); + sizes = (JsonArray) jsonNode.get("sizes"); } // format - ArrayNode formats = null; + JsonArray formats = null; if (jsonNode.get("formats") != null) { - formats = (ArrayNode) jsonNode.get("formats"); + formats = (JsonArray) jsonNode.get("formats"); } // rights @@ -458,7 +467,8 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso rightsIdentifier = element.get("rightsIdentifier").asText(); } - ContextualEntity license = new ContextualEntity.ContextualEntityBuilder() + ContextualEntity license = + new ContextualEntity.ContextualEntityBuilder() .addType("CreativeWork") .addProperty("name", licenseName) .setId(rightsUri) @@ -475,7 +485,6 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso Set funderSet = new HashSet<>(); Set grandSet = new HashSet<>(); if (jsonNode.get(fundingNameString) != null) { - for (var el : jsonNode.get(fundingNameString)) { var funderId = el.get("funderIdentifierType"); PersonEntity funder; @@ -483,10 +492,13 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso funder = OrcidProvider.getPerson(funderId.asText()); } else { funder = new PersonEntity.PersonEntityBuilder() - .addProperty("name", el.get("funderName")) - .setId(el.get("funderIdentifier") == null - ? null : el.get("funderIdentifier").asText()) - .build(); + .addProperty("name", el.get("funderName")) + .setId( + el.get("funderIdentifier") == null + ? null + : el.get("funderIdentifier").asText() + ) + .build(); } funderSet.add(funder); entityList.add(funder); @@ -496,7 +508,8 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso var awardTitle = el.get("awardTitle"); var awardNumber = el.get("awardNumber"); if (awardNumber != null || awardTitle != null || awardUri != null) { - ContextualEntity grant = new ContextualEntity.ContextualEntityBuilder() + ContextualEntity grant = + new ContextualEntity.ContextualEntityBuilder() .addType("Grant") .setId(awardUri == null ? null : awardUri.asText()) .addProperty("name", awardTitle) @@ -520,53 +533,55 @@ private static List getEntitiesFromDataCiteResource(JsonNode jso } } DataEntity dataEntity = new DataEntity.DataEntityBuilder() - .setId(id) - .addType(type) - .addProperty("title", title) - .addProperty("publicationYear", publicationYear) - .addIdProperty("publisher", publisher) - .addIdFromCollectionOfEntities("creator", creatorsList) - .addIdFromCollectionOfEntities("contributor", contributorsList) - .addIdFromCollectionOfEntities("spatialCoverage", geoSet) - .addIdFromCollectionOfEntities("license", licenses) - .addIdFromCollectionOfEntities("funder", funderSet) - .addIdFromCollectionOfEntities("funding", grandSet) - .addIdFromCollectionOfEntities("isPartOf", IsPartOf) - .addIdFromCollectionOfEntities("citation", citation) - .addIdFromCollectionOfEntities("hasPart", hasPart) - .addIdFromCollectionOfEntities("isBasedOn", IsBasedOn) - .addIdFromCollectionOfEntities("sameAs", sameAs) - .addProperty("description", descriptionArray) - .addProperty("dateCreated", dateCreated) - .addProperty("dateModified", dateModified) - .addProperty(PROPTERY_DATE_PUBLISHED, datePublished) - .addProperty("keywords", keyWordsArray) - .addProperty("inLanguage", language) - .addProperty("identifier", alternate) - .addProperty("size", sizes) - .addProperty("encodingFormat", formats) - .addProperty("version", jsonNode.get("version")) - .build(); + .setId(id) + .addType(type) + .addProperty("title", title) + .addProperty("publicationYear", publicationYear) + .addIdProperty("publisher", publisher) + .addIdFromCollectionOfEntities("creator", creatorsList) + .addIdFromCollectionOfEntities("contributor", contributorsList) + .addIdFromCollectionOfEntities("spatialCoverage", geoSet) + .addIdFromCollectionOfEntities("license", licenses) + .addIdFromCollectionOfEntities("funder", funderSet) + .addIdFromCollectionOfEntities("funding", grandSet) + .addIdFromCollectionOfEntities("isPartOf", IsPartOf) + .addIdFromCollectionOfEntities("citation", citation) + .addIdFromCollectionOfEntities("hasPart", hasPart) + .addIdFromCollectionOfEntities("isBasedOn", IsBasedOn) + .addIdFromCollectionOfEntities("sameAs", sameAs) + .addProperty("description", descriptionArray) + .addProperty("dateCreated", dateCreated) + .addProperty("dateModified", dateModified) + .addProperty(PROPTERY_DATE_PUBLISHED, datePublished) + .addProperty("keywords", keyWordsArray) + .addProperty("inLanguage", language) + .addProperty("identifier", alternate) + .addProperty("size", sizes) + .addProperty("encodingFormat", formats) + .addProperty("version", jsonNode.get("version")) + .build(); entityList.add(dataEntity); return entityList; } // here one can map also all the other properties of relatedItem if necessary - private static DataEntity getFromRelatedItem(JsonNode relatedItem) { + private static DataEntity getFromRelatedItem(JsonValue relatedItem) { var relatedId = relatedItem.get("relatedIdentifier"); String relatedIdType = relatedItem.get("relatedIdentifierType").asText(); String relationTypeString = relatedItem.get("relationType").asText(); return new DataEntity.DataEntityBuilder() - .addType("CreativeWork") - .setId(relatedId == null ? null : relatedId.asText()) - .addProperty("description", relatedIdType) - .addProperty("keywords", relationTypeString) - .build(); + .addType("CreativeWork") + .setId(relatedId == null ? null : relatedId.asText()) + .addProperty("description", relatedIdType) + .addProperty("keywords", relationTypeString) + .build(); } - private static AbstractEntity addPersonToListOfEntities(JsonNode personNode, - Collection entities) { + private static AbstractEntity addPersonToListOfEntities( + JsonValue personNode, + Collection entities + ) { // if there is an Orcid provider use it to get the entity directly from orcid var creatorIdentifiers = personNode.get("nameIdentifiers"); PersonEntity person = null; @@ -574,7 +589,9 @@ private static AbstractEntity addPersonToListOfEntities(JsonNode personNode, if (creatorIdentifiers != null && !creatorIdentifiers.isEmpty()) { for (var identifier : creatorIdentifiers) { if (identifier.get("nameIdentifierScheme").asText().equals("ORCID")) { - person = OrcidProvider.getPerson(identifier.get("nameIdentifier").asText()); + person = OrcidProvider.getPerson( + identifier.get("nameIdentifier").asText() + ); } else { personUrl = identifier.get("nameIdentifier").asText(); } @@ -583,11 +600,11 @@ private static AbstractEntity addPersonToListOfEntities(JsonNode personNode, if (person == null) { person = new PersonEntity.PersonEntityBuilder() - .addProperty("name", personNode.get("creatorName")) - .addProperty("givenName", personNode.get("givenName")) - .addProperty("familyName", personNode.get("familyName")) - .addProperty("url", personUrl) - .build(); + .addProperty("name", personNode.get("creatorName")) + .addProperty("givenName", personNode.get("givenName")) + .addProperty("familyName", personNode.get("familyName")) + .addProperty("url", personUrl) + .build(); } // get the person affiliations @@ -605,17 +622,22 @@ private static AbstractEntity addPersonToListOfEntities(JsonNode personNode, organization = RorProvider.getOrganization(affiliationId.asText()); } else { organization = new OrganizationEntity.OrganizationEntityBuilder() - .setId(affiliationId.asText()) - .addProperty("name", affiliationName) - .build(); + .setId(affiliationId.asText()) + .addProperty("name", affiliationName) + .build(); } organizationEntitySet.add(organization); } } - person.addIdListProperties("affiliation", - organizationEntitySet.stream().map(AbstractEntity::getId).collect(Collectors.toList())); + person.addIdListProperties( + "affiliation", + organizationEntitySet + .stream() + .map(AbstractEntity::getId) + .collect(Collectors.toList()) + ); entities.addAll(organizationEntitySet); entities.add(person); return person; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java similarity index 74% rename from src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java index 4a99ee8a..eb54569f 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java @@ -3,21 +3,17 @@ import com.apicatalog.jsonld.JsonLd; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.document.JsonDocument; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.RoCrate.RoCrateBuilder; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Optional; - import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -45,12 +41,12 @@ private ImportFromZenodo() {} * @return the created crate. */ public static Crate createCrateWithItem( - String url, - Optional name, - Optional description, - Optional datePublished, - Optional licenseId) { - + String url, + Optional name, + Optional description, + Optional datePublished, + Optional licenseId + ) { RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); name.ifPresent(builder::addName); description.ifPresent(builder::addDescription); @@ -67,40 +63,48 @@ public static void addZenodoToCrate(String ulr, Crate crate) { } private static void addToCrateFromZotero(String url, Crate crate) { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet requestZenodo = new HttpGet(url); // HttpGet requestSchema= new HttpGet("https://www.researchobject.org/ro-crate/1.1/context.jsonld"); - HttpGet requestSchema = new HttpGet("https://schema.org/docs/jsonldcontext.json"); + HttpGet requestSchema = new HttpGet( + "https://schema.org/docs/jsonldcontext.json" + ); requestZenodo.addHeader(HttpHeaders.ACCEPT, "application/ld+json"); - ObjectNode jsonNode; + JsonObject jsonNode; String mainId; try { CloseableHttpResponse response = httpClient.execute(requestZenodo); var stream = response.getEntity().getContent(); // get the item of the main entity - jsonNode = (ObjectNode) objectMapper.readTree(stream); + jsonNode = (JsonObject) Json.parse(stream); mainId = jsonNode.get("@id").asText(); - final var el = JsonLd.flatten(JsonDocument.of( - new ByteArrayInputStream( - jsonNode.toString().getBytes(StandardCharsets.UTF_8)))).get(); + final var el = JsonLd.flatten( + JsonDocument.of( + new ByteArrayInputStream( + jsonNode.toString().getBytes(StandardCharsets.UTF_8) + ) + ) + ).get(); CloseableHttpResponse schema = httpClient.execute(requestSchema); - ObjectNode doc = (ObjectNode) objectMapper.readTree(schema.getEntity().getContent()); - var con = (ObjectNode) doc.get("@context"); + JsonObject doc = (JsonObject) Json.parse(schema.getEntity().getContent()); + var con = (JsonObject) doc.get("@context"); con.remove("type"); con.remove("id"); doc.set("@context", con); var finalVersion = JsonLd.compact( - JsonDocument.of(el), - JsonDocument.of(new ByteArrayInputStream( - doc.toString().getBytes(StandardCharsets.UTF_8)))).get(); - - - jsonNode = (ObjectNode) objectMapper.readTree(finalVersion.toString()); + JsonDocument.of(el), + JsonDocument.of( + new ByteArrayInputStream( + doc.toString().getBytes(StandardCharsets.UTF_8) + ) + ) + ).get(); + + jsonNode = (JsonObject) Json.parse(finalVersion.toString()); } catch (IOException | JsonLdError e) { e.printStackTrace(); return; @@ -112,12 +116,14 @@ private static void addToCrateFromZotero(String url, Crate crate) { for (var entity : graph) { if (entity.get("@id").asText().equals(mainId)) { var dataEntity = new DataEntity.DataEntityBuilder() - .setAllUnsafe((ObjectNode) entity).build(); + .setAllUnsafe((JsonObject) entity) + .build(); crate.addDataEntity(dataEntity); } else { // here we have to think of a way to differentiate between data and contextual entities. var contextualEntity = new ContextualEntity.ContextualEntityBuilder() - .setAllUnsafe((ObjectNode) entity).build(); + .setAllUnsafe((JsonObject) entity) + .build(); crate.addContextualEntity(contextualEntity); } } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java new file mode 100644 index 00000000..910c3d10 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java @@ -0,0 +1,94 @@ +package edu.kit.datamanager.ro_crate.externalproviders.organizationprovider; + +import edu.kit.datamanager.ro_crate.entities.contextual.OrganizationEntity; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class providing possibility to import organization entities from ror.com. + */ +public class RorProvider { + + private static final Logger logger = LoggerFactory.getLogger( + RorProvider.class + ); + + private RorProvider() {} + + /** + * The method that parses a ror entry to a crate entity. + * + * @param url the url of the ror entry. + * @return the created Organization entity. + */ + public static OrganizationEntity getOrganization(String url) { + if (!url.startsWith("https://ror.org/")) { + throw new IllegalArgumentException("Should provide ror url"); + } + String newUrl = + "https://api.ror.org/v2/organizations/" + + url.replaceAll("https://ror.org/", ""); + HttpGet request = new HttpGet(newUrl); + + try ( + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = httpClient.execute(request) + ) { + boolean isError = + response.getStatusLine().getStatusCode() != HttpStatus.SC_OK; + if (isError) { + String errorMessage = String.format( + "Identifier not found: %s", + response.getStatusLine().toString() + ); + logger.error(errorMessage); + return null; + } + JsonObject jsonNode = (JsonObject) Json.parse( + response.getEntity().getContent() + ); + + return new OrganizationEntity.OrganizationEntityBuilder() + .setId(jsonNode.path("id").asText()) + .addProperty("name", getOrganizationNameV2(jsonNode.path("names"))) + .addProperty("email", jsonNode.path("email_address")) + .addProperty("url", jsonNode.path("id").asText()) + .build(); + } catch (IOException e) { + String errorMessage = String.format("IO error: %s", e.getMessage()); + logger.error(errorMessage); + } + return null; + } + + private static String getOrganizationNameV2(JsonValue node) { + if (node.isArray()) { + for (JsonValue n : node) { + if (n.has("types") && n.path("types").isArray()) { + List l = new ArrayList<>(); + for (JsonValue type : n.path("types")) { + l.add(type.asText()); + } + if (l.contains("ror_display")) { + return n.path("value").asText(); + } + } + } + //fallback + return node.path(0).path("value").asText(); + } else { + return node.asText(); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java similarity index 67% rename from src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java index 17207993..0b3da65b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java @@ -1,13 +1,10 @@ package edu.kit.datamanager.ro_crate.externalproviders.personprovider; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.kit.datamanager.ro_crate.entities.contextual.PersonEntity; import edu.kit.datamanager.ro_crate.entities.validation.EntityValidation; -import edu.kit.datamanager.ro_crate.entities.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.validation.Validation; import java.io.IOException; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; @@ -48,23 +45,32 @@ public static PersonEntity getPerson(String url) { CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(request); ) { - boolean isError = response.getStatusLine().getStatusCode() != HttpStatus.SC_OK; - String receivedMimeType = ContentType.parse(response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).getMimeType(); - boolean isUnexpectedFormat = response.containsHeader(HttpHeaders.CONTENT_TYPE) - && !receivedMimeType.equals("application/ld+json"); + boolean isError = + response.getStatusLine().getStatusCode() != HttpStatus.SC_OK; + String receivedMimeType = ContentType.parse( + response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue() + ).getMimeType(); + boolean isUnexpectedFormat = + response.containsHeader(HttpHeaders.CONTENT_TYPE) && + !receivedMimeType.equals("application/ld+json"); if (isError || isUnexpectedFormat) { - String errorMessage = String.format("Identifier not found: %s", response.getStatusLine().toString()); + String errorMessage = String.format( + "Identifier not found: %s", + response.getStatusLine().toString() + ); logger.error(errorMessage); return null; } - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode = objectMapper.readValue(response.getEntity().getContent(), - ObjectNode.class); + JsonObject jsonNode = (JsonObject) Json.parse( + response.getEntity().getContent() + ); jsonNode.remove("@reverse"); jsonNode.remove("@context"); - ObjectNode node = objectMapper.createObjectNode(); - EntityValidation entityValidation = new EntityValidation(new JsonSchemaValidation()); + JsonObject node = Json.objectNode(); + EntityValidation entityValidation = new EntityValidation( + Validation.entityStrategy() + ); var itr = jsonNode.fields(); while (itr.hasNext()) { var element = itr.next(); diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java new file mode 100644 index 00000000..5552972c --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java @@ -0,0 +1,203 @@ +package edu.kit.datamanager.ro_crate.hierarchy; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; +import edu.kit.datamanager.ro_crate.util.FileSystemUtil; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class HierarchyRecognition { + + protected final Crate crate; + protected final HierarchyRecognitionConfig config; + + public HierarchyRecognition(Crate crate, HierarchyRecognitionConfig config) { + this.crate = crate; + this.config = config; + } + + public HierarchyRecognitionResult buildHierarchy() { + HierarchyRecognitionResult result = new HierarchyRecognitionResult(); + + try { + // Get all data entities to process + Set allEntities = this.crate.getAllDataEntities(); + allEntities.add(crate.getRootDataEntity()); + + // Filter entities with file path IDs (not URLs, DOIs, etc.) + Map pathEntities = new HashMap<>(); + for (DataEntity entity : allEntities) { + String id = entity.getId(); + if (FileSystemUtil.isFilePath(id)) { + pathEntities.put(id, entity); + } else { + result.addSkippedEntity(entity); + } + } + + // Validate hierarchy before making changes + if (!HierarchyRecognition.validateHierarchy(pathEntities, result)) { + return result; + } + + // Create missing intermediate entities if configured + if (config.createMissingIntermediateEntities()) { + this.createMissingIntermediateEntities(pathEntities, result); + } + + // Clear existing relationships if configured + if (config.removeExistingConnections()) { + this.clearExistingRelationships(pathEntities); + } + + // Build hierarchy relationships + this.buildHierarchyRelationships(pathEntities, config, result); + + return result; + } catch (Exception e) { + result.addError( + "Unexpected error during hierarchy recognition: " + e.getMessage() + ); + return result; + } + } + + /** + * Validates that the hierarchy is consistent (no files containing other files/folders). + * + * @param pathEntities map of path IDs to DataEntities + * @param result builder to collect errors + * @return true if valid, false if invalid hierarchy detected + */ + protected static boolean validateHierarchy( + Map pathEntities, + HierarchyRecognitionResult result + ) { + for (Map.Entry entry : pathEntities.entrySet()) { + String childId = entry.getKey(); + String parentPath = FileSystemUtil.getParentPath(childId); + if (parentPath == null || parentPath.equals("./")) { + continue; + } + + // Check both with and without trailing slash since files don't have slash but folders do + DataEntity parentEntity = pathEntities.get(parentPath); + if (parentEntity == null) { + parentEntity = pathEntities.get(parentPath + "/"); + } + + if (parentEntity == null) { + continue; + } + + // Check for invalid hierarchy: file cannot contain another file/folder + if (parentEntity.getTypes().contains("File")) { + result.addError( + "Invalid hierarchy: file '" + + parentEntity.getId() + + "' cannot contain '" + + childId + + "'" + ); + return false; + } + } + return true; + } + + /** + * Creates missing intermediate DataSetEntity instances for folder paths. + * + * @param pathEntities map of path IDs to DataEntities + * @param result builder to collect created entities + */ + protected void createMissingIntermediateEntities( + Map pathEntities, + HierarchyRecognitionResult result + ) { + Set missingPaths = new HashSet<>(); + + // Find all missing intermediate paths + for (String path : pathEntities.keySet()) { + String parentPath = FileSystemUtil.getParentPath(path); + while (parentPath != null && !parentPath.equals("./")) { + String folderPath = parentPath + "/"; + final boolean containsParent = pathEntities.containsKey(parentPath); + final boolean containsFolder = pathEntities.containsKey(folderPath); + if (!containsParent && !containsFolder) { + missingPaths.add(folderPath); + } + parentPath = FileSystemUtil.getParentPath(parentPath); + } + } + + // Create missing DataSetEntity instances + for (String missingPath : missingPaths) { + DataSetEntity newEntity = new DataSetEntity.DataSetBuilder() + .setId(missingPath) + .addProperty("name", "Auto-generated folder: " + missingPath) + .build(); + + this.crate.addDataEntity(newEntity); + pathEntities.put(missingPath, newEntity); + result.addCreatedEntity(newEntity); + } + } + + protected void buildHierarchyRelationships( + Map pathEntities, + HierarchyRecognitionConfig config, + HierarchyRecognitionResult result + ) { + for (Map.Entry entry : pathEntities.entrySet()) { + String childId = entry.getKey(); + DataEntity childEntity = entry.getValue(); + String parentPath = FileSystemUtil.getParentPath(childId); + if (parentPath == null) { + continue; + } + + // Check both with and without trailing slash since files don't have slash but folders do + DataEntity parentEntity = pathEntities.get(parentPath); + String actualParentId = parentPath; + + if (parentEntity == null) { + parentEntity = pathEntities.get(parentPath + "/"); + actualParentId = parentPath + "/"; + } + + if (parentEntity == null) { + continue; + } + + // Add hasPart relationship + if (parentEntity instanceof DataSetEntity) { + ((DataSetEntity) parentEntity).addToHasPart(childId); + result.addProcessedRelationship(actualParentId, childId); + } + + // Add isPartOf relationship if configured + if (config.createInverseRelationships()) { + childEntity.addProperty("isPartOf", actualParentId); + } + + // Remove from root if it has a parent that is not root + if (!parentPath.equals("./")) { + this.crate.getRootDataEntity().removeFromHasPart(childId); + } + } + } + + protected void clearExistingRelationships( + Map pathEntities + ) { + for (DataEntity entity : pathEntities.values()) { + if (entity instanceof DataSetEntity) { + ((DataSetEntity) entity).hasPart.clear(); + } + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java new file mode 100644 index 00000000..28eaa57c --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java @@ -0,0 +1,59 @@ +package edu.kit.datamanager.ro_crate.hierarchy; + +/** + * Configuration class for automatic hierarchy recognition. + * This class provides control over how the hierarchy recognition. + * + *

    + *
  • createMissingIntermediateEntities: Whether missing intermediate folder entities should be automatically created. Default: false
  • + *
  • createInverseRelationships: Whether isPartOf relationships should be added in addition to hasPart. Default: false
  • + *
  • removeExistingConnections: Whether hasPart relationships should be added (false) or remove existing relations in beforehand (true). Default: false
  • + *
+ */ +public record HierarchyRecognitionConfig( + boolean createMissingIntermediateEntities, + boolean createInverseRelationships, + boolean removeExistingConnections +) { + /** + * Creates a new configuration with default values. + *

+ * Default values: + *

    + *
  • createMissingIntermediateEntities: false
  • + *
  • createInverseRelationships: false
  • + *
  • removeExistingConnections: false
  • + *
+ */ + public HierarchyRecognitionConfig() { + this(false, false, false); + } + + public HierarchyRecognitionConfig withCreateMissingIntermediateEntities( + boolean value + ) { + return new HierarchyRecognitionConfig( + value, + this.createInverseRelationships, + this.removeExistingConnections + ); + } + + public HierarchyRecognitionConfig withSetInverseRelationships(boolean value) { + return new HierarchyRecognitionConfig( + this.createMissingIntermediateEntities, + value, + this.removeExistingConnections + ); + } + + public HierarchyRecognitionConfig withRemoveExistingConnections( + boolean value + ) { + return new HierarchyRecognitionConfig( + this.createMissingIntermediateEntities, + this.createInverseRelationships, + value + ); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java new file mode 100644 index 00000000..855cdbab --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java @@ -0,0 +1,56 @@ +package edu.kit.datamanager.ro_crate.hierarchy; + +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; +import java.util.*; + +/** + * Result class containing information about the automatic hierarchy recognition operation. + * This class provides details about what was processed, created, and any issues encountered + * during the hierarchy recognition process. Always contains complete information about the + * operation result, including success/failure and any errors encountered. + */ +public record HierarchyRecognitionResult( + Set createdEntities, + Map> processedRelationships, + Set skippedEntities, + List warnings, + List errors +) { + HierarchyRecognitionResult() { + this( + new HashSet<>(), + new HashMap<>(), + new HashSet<>(), + new ArrayList<>(), + new ArrayList<>() + ); + } + + /** + * Whether there were no errors during the hierarchy recognition operation. + * + * @return true if the operation completed successfully (no errors), false otherwise. + */ + public boolean isSuccessful() { + return this.errors.isEmpty(); + } + + public void addSkippedEntity(DataEntity entity) { + this.skippedEntities.add(entity); + } + + public void addError(String errorMessage) { + this.errors.add(errorMessage); + } + + public void addCreatedEntity(DataSetEntity newEntity) { + this.createdEntities.add(newEntity); + } + + public void addProcessedRelationship(String from, String to) { + this.processedRelationships + .computeIfAbsent(from, k -> new HashSet<>()) + .add(to); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java similarity index 90% rename from src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java index 5d8f119f..6758294e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java @@ -6,4 +6,4 @@ * class, which analyzes the paths of data entities and establishes appropriate * "hasPart" and "isPartOf" relationships to reflect the underlying file system structure. */ -package edu.kit.datamanager.ro_crate.hierarchy; \ No newline at end of file +package edu.kit.datamanager.ro_crate.hierarchy; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java new file mode 100644 index 00000000..02eabe6a --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java @@ -0,0 +1,107 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Static access point to the JSON operations of the neutral tree model. + * + *

The concrete {@link JsonAdapter} (Jackson 2 or Jackson 3) is discovered + * via {@link ServiceLoader}; exactly one adapter must be on the classpath. + * Shading users or special setups can override discovery with + * {@link #setAdapter}. + */ +public final class Json { + + private static volatile JsonAdapter adapter; + + private Json() {} + + public static JsonAdapter adapter() { + JsonAdapter result = adapter; + if (result == null) { + synchronized (Json.class) { + result = adapter; + if (result == null) { + adapter = result = discoverAdapter(); + } + } + } + return result; + } + + /** Overrides adapter discovery; intended for tests and shaded builds. */ + public static void setAdapter(JsonAdapter replacement) { + adapter = replacement; + } + + private static JsonAdapter discoverAdapter() { + return uniqueCandidate( + ServiceLoader.load(JsonAdapter.class).iterator(), + JsonAdapter.class + ); + } + + /** + * Returns the single candidate or throws an exception that names the + * backend artifacts to add or exclude, so users who do not know the module + * layout can fix the classpath themselves. + */ + static T uniqueCandidate(Iterator candidates, Class type) { + if (!candidates.hasNext()) { + throw new IllegalStateException( + "No " + + type.getSimpleName() + + " found on the classpath. Add a JSON " + + "backend as a dependency: edu.kit.datamanager:ro-crate-java " + + "(Jackson 2 backend) or edu.kit.datamanager:ro-crate-java-jackson3 " + + "(Jackson 3 backend)." + ); + } + T discovered = candidates.next(); + if (candidates.hasNext()) { + throw new IllegalStateException( + "Multiple " + + type.getSimpleName() + + " implementations found on the " + + "classpath; ro-crate-java supports only one JSON backend at a " + + "time. Keep the backend matching your Jackson version and exclude " + + "the other one, e.g. exclude edu.kit.datamanager:ro-crate-java " + + "(Jackson 2 backend) and add " + + "edu.kit.datamanager:ro-crate-java-jackson3 (Jackson 3 backend)." + ); + } + return discovered; + } + + public static JsonObject objectNode() { + return adapter().objectNode(); + } + + public static JsonArray arrayNode() { + return adapter().arrayNode(); + } + + public static JsonValue parse(String json) throws IOException { + return adapter().parse(json); + } + + public static JsonValue parse(File file) throws IOException { + return adapter().parse(file); + } + + public static JsonValue parse(InputStream stream) throws IOException { + return adapter().parse(stream); + } + + public static String write(JsonValue value) throws IOException { + return adapter().write(value); + } + + public static String writePretty(JsonValue value) throws IOException { + return adapter().writePretty(value); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java new file mode 100644 index 00000000..0a6fffad --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java @@ -0,0 +1,32 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * The SPI through which the Jackson-free core talks to a concrete JSON + * implementation (Jackson 2 or Jackson 3). + * + *

Implementations must provide an adapter on the classpath and register it + * via {@code META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter} + * (see {@link Json}). + */ +public interface JsonAdapter { + JsonObject objectNode(); + + JsonArray arrayNode(); + + /** Parses JSON text into the neutral tree model. */ + JsonValue parse(String json) throws IOException; + + JsonValue parse(File file) throws IOException; + + JsonValue parse(InputStream stream) throws IOException; + + /** Serializes a neutral tree to compact JSON text. */ + String write(JsonValue value) throws IOException; + + /** Serializes a neutral tree to pretty-printed JSON text. */ + String writePretty(JsonValue value) throws IOException; +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java new file mode 100644 index 00000000..9553dbb2 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java @@ -0,0 +1,119 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +/** + * The JSON array value: a mutable, order-preserving list of values. Mirrors + * Jackson's {@code ArrayNode} API for the operations the library uses. + */ +public final class JsonArray extends JsonValue { + + private final List items = new ArrayList<>(); + + /** @return a new empty array (the neutral equivalent of createArrayNode) */ + public static JsonArray create() { + return new JsonArray(); + } + + // -- construction / mutation ------------------------------------------------ + + public JsonArray add(JsonValue value) { + // mirrors Jackson's ArrayNode.add: a null argument becomes a null node + items.add(value == null ? JsonNull.INSTANCE : value); + return this; + } + + public JsonArray add(String value) { + return add(value == null ? JsonNull.INSTANCE : new JsonString(value)); + } + + public JsonArray add(long value) { + return add(new JsonNumber(value)); + } + + public JsonArray add(double value) { + return add(new JsonNumber(value)); + } + + public JsonArray add(boolean value) { + return add(JsonBoolean.of(value)); + } + + public JsonArray addAll(JsonArray other) { + items.addAll(other.items); + return this; + } + + /** @return the removed element (mirrors Jackson) */ + public JsonValue remove(int index) { + return items.remove(index); + } + + // -- access ----------------------------------------------------------------- + + public JsonValue get(int index) { + if (index < 0 || index >= items.size()) { + return null; // mirrors Jackson: out-of-bounds yields null + } + return items.get(index); + } + + /** @return the element, or {@link JsonMissing} when out of bounds */ + public JsonValue path(int index) { + JsonValue value = get(index); + return value == null ? JsonMissing.INSTANCE : value; + } + + @Override + public int size() { + return items.size(); + } + + public boolean isEmpty() { + return items.isEmpty(); + } + + @Override + public Iterator iterator() { + return List.copyOf(items).iterator(); + } + + @Override + public Stream valueStream() { + return List.copyOf(items).stream(); + } + + // -- tree operations -------------------------------------------------------- + + @Override + public JsonArray deepCopy() { + JsonArray copy = new JsonArray(); + for (JsonValue item : items) { + copy.add(item.deepCopy()); + } + return copy; + } + + @Override + public boolean isArray() { + return true; + } + + @Override + public String asText() { + return ""; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof JsonArray other && items.equals(other.items); + } + + @Override + public int hashCode() { + return items.hashCode(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java new file mode 100644 index 00000000..dfb21fb5 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java @@ -0,0 +1,52 @@ +package edu.kit.datamanager.ro_crate.json; + +/** The JSON boolean value. */ +public final class JsonBoolean extends JsonValue { + + public static final JsonBoolean TRUE = new JsonBoolean(true); + public static final JsonBoolean FALSE = new JsonBoolean(false); + + private final boolean value; + + private JsonBoolean(boolean value) { + this.value = value; + } + + public static JsonBoolean of(boolean value) { + return value ? TRUE : FALSE; + } + + public boolean getValue() { + return value; + } + + @Override + public boolean isValueNode() { + return true; + } + + @Override + public boolean isBoolean() { + return true; + } + + @Override + public String asText() { + return Boolean.toString(value); + } + + @Override + public JsonBoolean deepCopy() { + return this; // immutable + } + + @Override + public boolean equals(Object obj) { + return obj instanceof JsonBoolean other && other.value == value; + } + + @Override + public int hashCode() { + return Boolean.hashCode(value); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java new file mode 100644 index 00000000..173bc0ec --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java @@ -0,0 +1,48 @@ +package edu.kit.datamanager.ro_crate.json; + +/** + * The result of {@link JsonObject#path(String)} for absent keys, mirroring + * Jackson's {@code MissingNode}. + */ +public final class JsonMissing extends JsonValue { + + public static final JsonMissing INSTANCE = new JsonMissing(); + + private JsonMissing() {} + + @Override + public boolean isValueNode() { + return false; + } + + @Override + public boolean isMissing() { + return true; + } + + @Override + public String asText() { + return ""; + } + + /** Mirrors Jackson's {@code MissingNode.asText(default)}: the default. */ + @Override + public String asText(String defaultValue) { + return defaultValue; + } + + @Override + public JsonMissing deepCopy() { + return this; // immutable + } + + @Override + public boolean equals(Object obj) { + return obj instanceof JsonMissing; + } + + @Override + public int hashCode() { + return JsonMissing.class.hashCode(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java new file mode 100644 index 00000000..11fd51ee --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java @@ -0,0 +1,48 @@ +package edu.kit.datamanager.ro_crate.json; + +/** + * The JSON null value, mirroring Jackson's {@code NullNode}: + * {@link #asText()} returns the string {@code "null"}, while + * {@link #asText(String)} returns the given default. + */ +public final class JsonNull extends JsonValue { + + public static final JsonNull INSTANCE = new JsonNull(); + + private JsonNull() {} + + @Override + public boolean isValueNode() { + return true; + } + + @Override + public boolean isNull() { + return true; + } + + @Override + public String asText() { + return "null"; + } + + @Override + public String asText(String defaultValue) { + return defaultValue; + } + + @Override + public JsonNull deepCopy() { + return this; // immutable + } + + @Override + public boolean equals(Object obj) { + return obj instanceof JsonNull; + } + + @Override + public int hashCode() { + return JsonNull.class.hashCode(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java new file mode 100644 index 00000000..e2e44bb5 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java @@ -0,0 +1,61 @@ +package edu.kit.datamanager.ro_crate.json; + +/** + * The JSON number value. + * + *

The original Java number type is preserved (Integer, Long, Double, ...) + * so that serialization stays byte-identical to Jackson's output for the same + * parsed document. Number equality is type-sensitive, mirroring Jackson's + * node equality: {@code 1} (integer) and {@code 1.0} (double) are not equal. + */ +public final class JsonNumber extends JsonValue { + + private final Number value; + + public JsonNumber(Number value) { + if (value == null) { + throw new IllegalArgumentException("JSON numbers cannot be null"); + } + this.value = value; + } + + public Number getValue() { + return value; + } + + @Override + public boolean isValueNode() { + return true; + } + + @Override + public boolean isNumber() { + return true; + } + + @Override + public String asText() { + return String.valueOf(value); + } + + @Override + public JsonNumber deepCopy() { + return this; // immutable + } + + @Override + public boolean equals(Object obj) { + // mirrors Jackson's node equality: numbers are only equal when the Java + // number class matches (IntNode(1) != LongNode(1), != DoubleNode(1.0)) + return ( + obj instanceof JsonNumber other && + value.getClass().equals(other.value.getClass()) && + value.equals(other.value) + ); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java new file mode 100644 index 00000000..e6175ecf --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java @@ -0,0 +1,215 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * The JSON object value: a mutable, insertion-ordered map from names to + * values. Mirrors Jackson's {@code ObjectNode} API for the operations the + * library uses. + */ +public final class JsonObject extends JsonValue { + + private final List names = new ArrayList<>(); + private final List values = new ArrayList<>(); + + /** @return a new empty object (the neutral equivalent of createObjectNode) */ + public static JsonObject create() { + return new JsonObject(); + } + + // -- construction / mutation ------------------------------------------------ + + public JsonObject put(String name, JsonValue value) { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + int index = names.indexOf(name); + if (index >= 0) { + values.set(index, value); + } else { + names.add(name); + values.add(value); + } + return this; + } + + public JsonObject put(String name, String value) { + return put(name, value == null ? JsonNull.INSTANCE : new JsonString(value)); + } + + public JsonObject put(String name, int value) { + return put(name, new JsonNumber(value)); + } + + public JsonObject put(String name, long value) { + return put(name, new JsonNumber(value)); + } + + public JsonObject put(String name, double value) { + return put(name, new JsonNumber(value)); + } + + public JsonObject put(String name, float value) { + return put(name, new JsonNumber(value)); + } + + public JsonObject put(String name, boolean value) { + return put(name, JsonBoolean.of(value)); + } + + public JsonObject putNull(String name) { + return put(name, JsonNull.INSTANCE); + } + + /** Mirrors Jackson's {@code set}: like {@link #put}, but keeps null values. */ + public JsonObject set(String name, JsonValue value) { + return put(name, value == null ? JsonNull.INSTANCE : value); + } + + /** Copies all entries of {@code other} into this object, in their order. */ + public JsonObject setAll(JsonObject other) { + for (var entry : other.entries()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + // -- access ----------------------------------------------------------------- + + /** @return the value, or null when absent (mirrors Jackson's {@code get}) */ + public JsonValue get(String name) { + int index = names.indexOf(name); + return index < 0 ? null : values.get(index); + } + + /** @return the value, or {@link JsonMissing} when absent (mirrors {@code path}) */ + public JsonValue path(String name) { + JsonValue value = get(name); + return value == null ? JsonMissing.INSTANCE : value; + } + + public boolean has(String name) { + return names.contains(name); + } + + /** @return the removed value, or null when absent (mirrors Jackson) */ + public JsonValue remove(String name) { + int index = names.indexOf(name); + if (index < 0) { + return null; + } + names.remove(index); + return values.remove(index); + } + + public void remove(Collection namesToRemove) { + namesToRemove.forEach(this::remove); + } + + public int size() { + return names.size(); + } + + public boolean isEmpty() { + return names.isEmpty(); + } + + /** + * Property names in insertion order, mirroring Jackson's + * {@code fieldNames()} iterator. + */ + public Iterator fieldNames() { + return List.copyOf(names).iterator(); + } + + /** Property names as a list, in insertion order. */ + public List names() { + return List.copyOf(names); + } + + /** Entries in insertion order. */ + public Iterable> entries() { + return new ArrayList<>(namedValues()); + } + + /** + * Mirrors Jackson's {@code fields()}: an iterator over entries in insertion + * order. The iterator does not support removal. + */ + public Iterator> fields() { + return namedValues().iterator(); + } + + /** Property values in insertion order (mirrors Jackson node iteration). */ + @Override + public Iterator iterator() { + return List.copyOf(values).iterator(); + } + + @Override + public Stream valueStream() { + return List.copyOf(values).stream(); + } + + private List> namedValues() { + List> entries = new ArrayList<>(names.size()); + for (int i = 0; i < names.size(); i++) { + entries.add( + new AbstractMap.SimpleImmutableEntry<>(names.get(i), values.get(i)) + ); + } + return entries; + } + + // -- tree operations -------------------------------------------------------- + + @Override + public JsonObject deepCopy() { + JsonObject copy = new JsonObject(); + for (int i = 0; i < names.size(); i++) { + copy.put(names.get(i), values.get(i).deepCopy()); + } + return copy; + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public String asText() { + return ""; + } + + @Override + public boolean equals(Object obj) { + // mirrors Jackson's ObjectNode.equals, which compares the underlying + // LinkedHashMap: order-insensitive, entry-wise structural equality + if (!(obj instanceof JsonObject other) || size() != other.size()) { + return false; + } + for (int i = 0; i < names.size(); i++) { + if (!values.get(i).equals(other.get(names.get(i)))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + // mirrors Map.hashCode semantics (order-insensitive) + int hash = 0; + for (int i = 0; i < names.size(); i++) { + hash += names.get(i).hashCode() ^ values.get(i).hashCode(); + } + return hash; + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java new file mode 100644 index 00000000..cc1ec2c6 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java @@ -0,0 +1,59 @@ +package edu.kit.datamanager.ro_crate.json; + +/** The JSON string value. */ +public final class JsonString extends JsonValue { + + private final String value; + + public JsonString(String value) { + if (value == null) { + throw new IllegalArgumentException("JSON strings cannot be null"); + } + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean isValueNode() { + return true; + } + + @Override + public boolean isString() { + return true; + } + + @Override + public boolean isTextual() { + return true; + } + + /** Mirrors Jackson's {@code TextNode.isEmpty()}. */ + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public String asText() { + return value; + } + + @Override + public JsonString deepCopy() { + return this; // immutable + } + + @Override + public boolean equals(Object obj) { + return obj instanceof JsonString other && other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java new file mode 100644 index 00000000..1eec7e51 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java @@ -0,0 +1,173 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.stream.Stream; + +/** + * A JSON value of the neutral, Jackson-free tree model used inside + * ro-crate-java. + * + *

The API deliberately mirrors the subset of Jackson's {@code JsonNode} + * that the library uses (same method names and semantics), so that domain + * code can be ported mechanically. This is not a general purpose JSON + * library: only the operations the library needs are supported. + * + *

Values are mutable ({@link JsonObject} and {@link JsonArray} can be + * modified in place), preserve insertion order, and implement deep structural + * equality. + */ +public abstract class JsonValue implements Iterable { + + /** + * Returns a deep copy of this value. + * + * @return an independent copy; mutations of the copy do not affect this value + */ + public abstract JsonValue deepCopy(); + + /** + * @return true for string, number, boolean and null values (mirrors Jackson) + */ + public boolean isValueNode() { + return false; + } + + public boolean isObject() { + return false; + } + + public boolean isArray() { + return false; + } + + public boolean isNull() { + return false; + } + + public boolean isString() { + return false; + } + + public boolean isNumber() { + return false; + } + + public boolean isBoolean() { + return false; + } + + /** @return true for the missing value returned by {@link JsonObject#path} */ + public boolean isMissing() { + return false; + } + + /** Mirrors Jackson's {@code isTextual()}. */ + public boolean isTextual() { + return false; + } + + /** + * Mirrors Jackson's {@code get(String)}: null when absent or when this is + * not an object. + */ + public JsonValue get(String name) { + return null; + } + + /** + * Mirrors Jackson's {@code get(int)}: null when out of bounds or when this + * is not an array. + */ + public JsonValue get(int index) { + return null; + } + + /** Mirrors Jackson's {@code has(String)}: false unless this is an object. */ + public boolean has(String name) { + return false; + } + + /** Mirrors Jackson's {@code size()}: 0 unless this is a container. */ + public int size() { + return 0; + } + + /** Mirrors Jackson's {@code isEmpty()}: containers by size, strings by length. */ + public boolean isEmpty() { + return size() == 0; + } + + /** + * Mirrors Jackson's {@code path(String)} on non-container nodes: always the + * missing value. Containers override this. + */ + public JsonValue path(String name) { + return JsonMissing.INSTANCE; + } + + /** + * Mirrors Jackson's {@code path(int)} on non-container nodes: always the + * missing value. Arrays override this. + */ + public JsonValue path(int index) { + return JsonMissing.INSTANCE; + } + + /** + * Mirrors Jackson's {@code fields()}: an entry iterator; empty for + * non-container nodes. + */ + public Iterator> fields() { + return Collections.emptyIterator(); + } + + /** + * Mirrors Jackson's {@code valueStream()}: scalars stream themselves, + * containers stream their values/elements. + */ + public Stream valueStream() { + return Stream.of(this); + } + + /** + * Text representation, mirroring Jackson's {@code asText()} semantics: + * value nodes return their string form, containers return the empty string. + */ + public abstract String asText(); + + /** + * Mirrors Jackson's {@code asText(String defaultValue)}: returns + * {@code defaultValue} when {@link #asText()} would return null, otherwise + * the same as {@link #asText()}. + */ + public String asText(String defaultValue) { + String text = asText(); + return text == null ? defaultValue : text; + } + + /** + * Mirrors Jackson's node iteration (verified against Jackson 2.22.1): + * arrays iterate their elements, objects iterate their values, scalars + * iterate nothing. + */ + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + /** Compact JSON representation, delegated to the active {@link JsonAdapter}. */ + @Override + public String toString() { + try { + return Json.write(this); + } catch (IOException exception) { + throw new java.io.UncheckedIOException( + "Serialization of the JSON tree failed", + exception + ); + } + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java similarity index 92% rename from src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java index 7639ef84..b9b7feca 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java @@ -1,11 +1,9 @@ package edu.kit.datamanager.ro_crate.payload; -import com.fasterxml.jackson.databind.node.ArrayNode; - 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; - +import edu.kit.datamanager.ro_crate.json.JsonArray; import java.util.Collection; import java.util.Set; @@ -38,7 +36,7 @@ public interface CratePayload { Set getAllContextualEntities(); - ArrayNode getEntitiesMetadata(); + JsonArray getEntitiesMetadata(); void removeEntityById(String id); } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java similarity index 78% rename from src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java index bc76d6a2..5db63a61 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java @@ -1,15 +1,11 @@ package edu.kit.datamanager.ro_crate.payload; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - 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; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; - import java.util.*; import java.util.stream.Collectors; @@ -22,8 +18,8 @@ public class RoCratePayload implements CratePayload { private final HashMap contextualEntities; /** - * A map containing for each entity id a set of ids of entities that are linked to it. - * This is used to make the removal of entities from the crate faster. + * A map containing for each entity id a set of ids of entities that are linked to it. + * This is used to make the removal of entities from the crate faster. */ private final HashMap> associatedItems; @@ -123,15 +119,13 @@ public Set getAllContextualEntities() { } @Override - public ArrayNode getEntitiesMetadata() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - - ArrayNode node = objectMapper.createArrayNode(); + public JsonArray getEntitiesMetadata() { + JsonArray node = Json.arrayNode(); for (DataEntity ent : this.getAllDataEntities()) { - node.add(objectMapper.convertValue(ent, ObjectNode.class)); + node.add(ent.toJson()); } for (ContextualEntity ent : this.getAllContextualEntities()) { - node.add(objectMapper.convertValue(ent, ObjectNode.class)); + node.add(ent.toJson()); } return node; } @@ -144,16 +138,19 @@ public void removeEntityById(String id) { } private void removeAllOccurrencesOf(String entityId) { - this.getEntitiesByIds(this.associatedItems.get(entityId)) - .forEach(entity -> JsonUtilFunctions.removeFieldsWith(entityId, entity.getProperties())); + this.getEntitiesByIds(this.associatedItems.get(entityId)).forEach(entity -> + JsonUtilFunctions.removeFieldsWith(entityId, entity.getProperties()) + ); } private Set getEntitiesByIds(Collection ids) { - if (ids == null) { return new HashSet<>(); } - return ids.stream() - .map(this::getEntityById) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + if (ids == null) { + return new HashSet<>(); + } + return ids + .stream() + .map(this::getEntityById) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } - } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java new file mode 100644 index 00000000..bb0db62d --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java @@ -0,0 +1,99 @@ +package edu.kit.datamanager.ro_crate.preview; + +import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import org.apache.commons.io.FileUtils; + +/** + * The default preview should use the + * rochtml tool + * for creating a simple preview file. + * + * @author Nikola Tzotchev on 6.2.2022 г. + * @version 1 + */ +public class AutomaticPreview implements CratePreview { + + public AutomaticPreview() {} + + @Override + public void saveAllToZip(ZipFile zipFile) throws IOException { + if (PreviewGenerator.isRochtmlAvailable()) { + // extract the .json file so we can run the "rochtml" tool on it" + try { + try { + zipFile.extractFile("ro-crate-metadata.json", "temp"); + } catch (ZipException ex) { + throw new IOException( + "ro-crate-metadata.json not found in provided ZIP.", + ex + ); + } + if (PreviewGenerator.isRochtmlAvailable()) { + PreviewGenerator.generatePreview("temp"); + zipFile.addFile("temp/ro-crate-preview.html"); + } + } finally { + try { + FileUtils.deleteDirectory(new File("temp")); + } catch (IOException ex) { + //ignore + } + } + } else { + new CustomPreview().saveAllToZip(zipFile); + } + } + + @Override + public void saveAllToFolder(File folder) throws IOException { + if (folder == null || !folder.exists()) { + throw new IOException( + "Preview target folder " + folder + " does not exist." + ); + } + + if (PreviewGenerator.isRochtmlAvailable()) { + PreviewGenerator.generatePreview(folder.getAbsolutePath()); + } else { + new CustomPreview().saveAllToFolder(folder); + } + } + + @Override + public void saveAllToStream(String metadata, ZipOutputStream stream) + throws IOException { + if (PreviewGenerator.isRochtmlAvailable()) { + try { + FileUtils.forceMkdir(new File("temp")); + try ( + FileWriter writer = new FileWriter("temp/ro-crate-metadata.json") + ) { + writer.write(metadata); + writer.flush(); + } + if (PreviewGenerator.isRochtmlAvailable()) { + PreviewGenerator.generatePreview("temp"); + ZipStreamUtil.addFileToZipStream( + stream, + new File("temp/ro-crate-preview.html"), + "ro-crate-preview.html" + ); + } + } finally { + try { + FileUtils.deleteDirectory(new File("temp")); + } catch (IOException ex) { + //ignore + } + } + } else { + new CustomPreview().saveAllToStream(metadata, stream); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java new file mode 100644 index 00000000..4983fdda --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java @@ -0,0 +1,108 @@ +package edu.kit.datamanager.ro_crate.preview; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.writer.CrateWriter; +import edu.kit.datamanager.ro_crate.writer.WriteFolderStrategy; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.slf4j.LoggerFactory; + +/** + * Interface for the ROCrate preview. This manages the human-readable + * representation of a crate. + * + * @author Nikola Tzotchev on 6.2.2022 г. + * @author jejkal + * @version 2 + */ +public interface CratePreview { + /** + * Generate a preview of the crate and store it into the given target directory. + * It is the caller's responsibility to handle, e.g. delete after use, the result + * (The caller takes ownership of the result). + *

+ * IMPORTANT NOTE: This method currently has a default implementation that relies + * on deprecated methods. In future, you will have to implement this method directly. + * + * @param crate the crate to generate a preview for. + * @param targetDir the target directory to store the preview in, + * owned by the caller. + * @throws IOException if an error occurs while generating the preview. + */ + default void generate(Crate crate, File targetDir) throws IOException { + // disable preview generation to avoid recursion, + // 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(null) + .save(crate, targetDir.getAbsolutePath()); + this.saveAllToFolder(targetDir); + try (var stream = Files.list(targetDir.toPath())) { + stream + .filter( + path -> !path.getFileName().toString().equals("ro-crate-preview.html") + ) + .filter( + path -> + !path.getFileName().toString().equals("ro-crate-preview_files") + ) + .forEach(path -> { + try { + if (Files.isDirectory(path)) { + FileUtils.deleteDirectory(path.toFile()); + } else { + Files.delete(path); + } + } catch (IOException e) { + // Silently ignore deletion errors + LoggerFactory.getLogger(CratePreview.class).error( + "Failed to delete temporary file {}", + path, + e + ); + } + }); + } + } + + /** + * Takes a crate in form of a zip file and generates a preview of it, + * which will be stored within the crate. + * + * @param zipFile the zip file with the crate, which should receive a preview. + * @throws IOException if an error occurs while saving the preview + * + * @deprecated Use {@link #generate(Crate, File)} instead. + */ + @Deprecated(since = "2.1.0", forRemoval = true) + void saveAllToZip(ZipFile zipFile) throws IOException; + + /** + * Saves the preview, given by the folder, into the given folder. + * + * @param folder the folder (containing a crate) to save the preview in. + * @throws IOException if an error occurs while saving the preview. + * + * @deprecated Use {@link #generate(Crate, File)} instead. + */ + @Deprecated(since = "2.1.0", forRemoval = true) + void saveAllToFolder(File folder) throws IOException; + + /** + * Saves the preview, given by the metadata, into the given stream. + * + * @param metadata the metadata of the crate to save the preview in. + * @param stream the stream to save the preview in. + * @throws IOException if an error occurs while saving the preview. + * + * @deprecated Use {@link #generate(Crate, File)} instead. + */ + @Deprecated(since = "2.1.0", forRemoval = true) + void saveAllToStream(String metadata, ZipOutputStream stream) + throws IOException; +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java new file mode 100644 index 00000000..61c4c67c --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java @@ -0,0 +1,249 @@ +package edu.kit.datamanager.ro_crate.preview; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class generates a custom preview without requiring external + * dependencies, i.e., rochtml. Therefore, the FreeMarker template located under + * resources/templates/custom_preview.ftl is used. + * + * @author jejkal + */ +public class CustomPreview implements CratePreview { + + private static final Logger logger = LoggerFactory.getLogger( + CustomPreview.class + ); + + private Template template = null; + + public CustomPreview() { + Configuration cfg = new Configuration(Configuration.VERSION_2_3_34); + cfg.setClassForTemplateLoading(CustomPreview.class, "/"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + try { + template = cfg.getTemplate("templates/custom_preview.ftl"); + } catch (IOException ex) { + logger.error("Failed to read template for CustomPreview.", ex); + } + } + + private CustomPreviewModel mapFromJson(String metadata) throws IOException { + JsonValue root = Json.parse(metadata); + JsonValue graph = root.get("@graph"); + CustomPreviewModel.ROCrate crate = new CustomPreviewModel.ROCrate(); + List datasets = new ArrayList<>(); + List files = new ArrayList<>(); + + if (graph.isArray()) { + for (JsonValue node : graph) { + String id = node.path("@id").asText(); + List types = new LinkedList<>(); + if (node.path("@type").isArray()) { + for (JsonValue type : node.path("@type")) { + types.add(type.asText()); + } + } else { + types.add(node.path("@type").asText()); + } + + if (types.contains("Dataset") && "./".equals(id)) { + crate.name = node.path("name").asText(); + crate.description = node.path("description").asText(null); + crate.type = "Dataset"; + crate.license = node + .path("license") + .path("@id") + .asText(node.path("license").asText(null)); + crate.datePublished = node.path("datePublished").asText(null); + crate.hasPart = new ArrayList<>(); + + for (JsonValue part : node.path("hasPart")) { + CustomPreviewModel.Part p = new CustomPreviewModel.Part(); + String tmpId = part.path("@id").asText(part.asText()); + p.id = tmpId; + p.name = tmpId; // Name will be replaced later + crate.hasPart.add(p); + } + } else if (types.contains("Dataset")) { + CustomPreviewModel.Dataset dataset = new CustomPreviewModel.Dataset(); + dataset.id = id; + dataset.name = node.path("name").asText(); + dataset.description = node.path("description").asText(); + datasets.add(dataset); + } else if (types.contains("File")) { + CustomPreviewModel.File file = new CustomPreviewModel.File(); + file.id = id; + file.name = node.path("name").asText(null); + file.description = node.path("description").asText(null); + file.contentSize = node.path("contentSize").asText(null); + file.encodingFormat = node.path("encodingFormat").asText(null); + files.add(file); + } + } + } + + // Update Part names using dataset and file lists + if (crate.hasPart != null) { + for (CustomPreviewModel.Part part : crate.hasPart) { + for (CustomPreviewModel.Dataset dataset : datasets) { + if (dataset.id.equals(part.id) && dataset.name != null) { + part.name = dataset.name; + } + } + for (CustomPreviewModel.File file : files) { + if (file.id.equals(part.id) && file.name != null) { + part.name = file.name; + } + } + } + } + + CustomPreviewModel model = new CustomPreviewModel(); + model.crate = crate; + model.datasets = datasets; + model.files = files; + return model; + } + + @Override + public void saveAllToZip(ZipFile zipFile) throws IOException { + if (template == null) { + throw new IOException( + "Preview template did not load. Unable to proceed." + ); + } + if (zipFile == null) { + throw new IOException("Argument zipFile must not be null."); + } + try { + zipFile.extractFile("ro-crate-metadata.json", "temp"); + } catch (ZipException ex) { + throw new IOException( + "ro-crate-metadata.json not found in provided ZIP.", + ex + ); + } + + String metadata = FileUtils.readFileToString( + new File("temp/ro-crate-metadata.json"), + "UTF-8" + ); + try { + Map dataModel = new HashMap<>(); + dataModel.put("crateModel", mapFromJson(metadata)); + + try ( + Writer out = new OutputStreamWriter( + new FileOutputStream("temp/ro-crate-preview.html") + ) + ) { + template.process(dataModel, out); + out.flush(); + } + zipFile.addFile("temp/ro-crate-preview.html"); + } catch (TemplateException ex) { + throw new IOException("Failed to generate preview.", ex); + } finally { + try { + FileUtils.deleteDirectory(new File("temp")); + } catch (IOException ex) { + //ignore + } + } + } + + @Override + public void saveAllToFolder(File folder) throws IOException { + if (template == null) { + throw new IOException( + "Preview template did not load. Unable to proceed." + ); + } + if (folder == null || !folder.exists()) { + throw new IOException( + "Preview target folder " + folder + " does not exist." + ); + } + String metadata = FileUtils.readFileToString( + new File(folder, "ro-crate-metadata.json"), + "UTF-8" + ); + try { + Map dataModel = new HashMap<>(); + dataModel.put("crateModel", mapFromJson(metadata)); + try ( + Writer out = new OutputStreamWriter( + new FileOutputStream(new File(folder, "ro-crate-preview.html")) + ) + ) { + template.process(dataModel, out); + out.flush(); + } + } catch (TemplateException ex) { + throw new IOException("Failed to generate preview.", ex); + } + } + + @Override + public void saveAllToStream(String metadata, ZipOutputStream stream) + throws IOException { + if (template == null) { + throw new IOException( + "Preview template did not load. Unable to proceed." + ); + } + try { + //prepare metadata for template + Map dataModel = new HashMap<>(); + dataModel.put("crateModel", mapFromJson(metadata)); + + //prepare output folder and writer + FileUtils.forceMkdir(new File("temp")); + //load and process template + try (FileWriter writer = new FileWriter("temp/ro-crate-preview.html")) { + //load and process template + template.process(dataModel, writer); + writer.flush(); + } + + ZipStreamUtil.addFileToZipStream( + stream, + new File("temp/ro-crate-preview.html"), + "ro-crate-preview.html" + ); + } catch (TemplateException ex) { + throw new IOException("Failed to generate preview.", ex); + } finally { + try { + FileUtils.deleteDirectory(new File("temp")); + } catch (IOException ex) { + //ignore + } + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java new file mode 100644 index 00000000..380251dc --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java @@ -0,0 +1,122 @@ +package edu.kit.datamanager.ro_crate.preview; + +import java.util.List; + +/** + * + * @author jejkal + */ +public class CustomPreviewModel { + + protected ROCrate crate; + protected List datasets; + protected List files; + + public ROCrate getCrate() { + return crate; + } + + public List getDatasets() { + return datasets; + } + + public List getFiles() { + return files; + } + + public static class ROCrate { + + protected String name; + protected String description; + protected String type; + protected String license; + protected String datePublished; + protected List hasPart; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getType() { + return type; + } + + public String getLicense() { + return license; + } + + public String getDatePublished() { + return datePublished; + } + + public List getHasPart() { + return hasPart; + } + } + + public static class Part { + + protected String id; + protected String name; + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + public static class Dataset { + + protected String id; + protected String name; + protected String description; + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + } + + public static class File { + + protected String id; + protected String name; + protected String description; + protected String contentSize; + protected String encodingFormat; + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getContentSize() { + return contentSize; + } + + public String getEncodingFormat() { + return encodingFormat; + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java new file mode 100644 index 00000000..2a729674 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java @@ -0,0 +1,92 @@ +package edu.kit.datamanager.ro_crate.preview; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class responsible for the generation of the human-readable representation of + * the metadata. For this to work the nodejs library rochtml has to be + * installed. This can be done using: npm install -g ro-crate-html-js or npm + * install ro-crate-html-js + */ +public class PreviewGenerator { + + private static Logger LOG = LoggerFactory.getLogger(PreviewGenerator.class); + private static final String command = "rochtml"; + + public static boolean isRochtmlAvailable() { + ProcessBuilder builder = new ProcessBuilder(); + // this is the equivalent of "rochtml dir/ro-crate-metadata.json" + // check if we are running on windows or unix + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + builder.command("cmd.exe", "/c", command); + } else { + builder.command("sh", "-c", command); + } + Process process; + try { + process = builder.start(); + int exitVal = process.waitFor(); + return exitVal == 0; + } catch (InterruptedException | IOException ex) {} + return false; + } + + /** + * The method that from the location of the crate generates the html file. + * + * @param location the location of the crate in the filesystem. + */ + public static void generatePreview(String location) { + if (!isRochtmlAvailable()) { + LOG.error( + "rochtml is not available. Please install rochtml using npm install -g ro-crate-html-js" + ); + return; + } + ProcessBuilder builder = new ProcessBuilder(); + // this is the equivalent of "rochtml dir/ro-crate-metadata.json" + // check if we are running on windows or unix + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + builder.command( + "cmd.exe", + "/c", + command + " " + location + "/ro-crate-metadata.json" + ); + } else { + builder.command( + "sh", + "-c", + command + " " + location + "/ro-crate-metadata.json" + ); + } + + Process process; + try { + process = builder.start(); + StringBuilder output = new StringBuilder(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()) + ); + + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + reader.close(); + int exitVal = process.waitFor(); + if (exitVal != 0) { + //abnormal... + throw new Exception("failed command"); + } + } catch (Exception e) { + LOG.error("Error while generating preview: {}", e.getMessage()); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java new file mode 100644 index 00000000..c4ef2b99 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java @@ -0,0 +1,91 @@ +package edu.kit.datamanager.ro_crate.preview; + +import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; +import java.io.File; +import java.io.IOException; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import net.lingala.zip4j.model.ZipParameters; +import org.apache.commons.io.FileUtils; + +/** + * This class adds a static preview to the crate, which consists of a + * metadataHtml file and a folder containing other files required to render + * metadataHtml. It will be put unchanged to the writer output, i.e., a zip + * file, folder, or stream. + * + * @author jejkal + */ +public class StaticPreview implements CratePreview { + + private final File metadataHtml; + private final File otherFiles; + + public StaticPreview(File metadataHtml, File otherFiles) { + this.metadataHtml = metadataHtml; + this.otherFiles = otherFiles; + } + + public StaticPreview(File metadataHtml) { + this.metadataHtml = metadataHtml; + this.otherFiles = null; + } + + @Override + public void saveAllToZip(ZipFile zipFile) throws IOException { + if (this.metadataHtml != null) { + ZipParameters zipParameters = new ZipParameters(); + zipParameters.setFileNameInZip("ro-crate-preview.html"); + zipFile.addFile(this.metadataHtml, zipParameters); + } + + if (this.otherFiles != null) { + zipFile.addFolder(this.otherFiles); + zipFile.renameFile( + this.otherFiles.getName() + "/", + "ro-crate-preview_files/" + ); + } + } + + @Override + public void saveAllToFolder(File folder) throws IOException { + if (folder == null || !folder.exists()) { + throw new IOException( + "Preview target folder " + folder + " does not exist." + ); + } + + if (this.metadataHtml != null) { + File fileInCrate = folder + .toPath() + .resolve("ro-crate-preview.html") + .toFile(); + FileUtils.copyFile(this.metadataHtml, fileInCrate); + } + if (this.otherFiles != null) { + File folderName = folder + .toPath() + .resolve("ro-crate-preview_files") + .toFile(); + FileUtils.copyDirectory(this.otherFiles, folderName); + } + } + + @Override + public void saveAllToStream(String metadata, ZipOutputStream stream) + throws IOException { + ZipStreamUtil.addFileToZipStream( + stream, + this.metadataHtml, + "ro-crate-preview.html" + ); + if (this.otherFiles != null) { + ZipStreamUtil.addFolderToZipStream( + stream, + this.otherFiles, + "ro-crate-preview_files" + ); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java new file mode 100644 index 00000000..e230e187 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -0,0 +1,422 @@ +package edu.kit.datamanager.ro_crate.reader; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; +import edu.kit.datamanager.ro_crate.context.RoCrateMetadataContext; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; +import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; +import edu.kit.datamanager.ro_crate.validation.Validation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class allows reading crates from the outside into the library in order + * to inspect or modify it. + *

+ * The constructor takes a strategy to support different ways of importing the + * crates. (from zip, folder, etc.). + *

+ * The reader considers "hasPart" and "isPartOf" properties and considers all + * entities (in-)directly connected to the root entity ("./") as DataEntities. + * + * @param the type of the location parameter + */ +public class CrateReader { + + private static final Logger logger = LoggerFactory.getLogger( + CrateReader.class + ); + + /** + * This is a private inner class that shall not be exposed. **Do not make it + * public or protected.** It serves only the purpose of unsafe operations + * while reading a crate and may be specific to this implementation. + */ + private static class RoCrateUnsafe extends RoCrate { + + public void addDataEntityWithoutRootHasPart(DataEntity entity) { + this.metadataContext.checkEntity(entity); + this.roCratePayload.addDataEntity(entity); + } + } + + /** + * If the number of JSON entities in the crate is larger than this number, + * parallelization will be used. + */ + private static final int PARALLELIZATION_THRESHOLD = 100; + + private static final String FILE_PREVIEW_FILES = "ro-crate-preview_files"; + private static final String FILE_PREVIEW_HTML = "ro-crate-preview.html"; + private static final String FILE_METADATA_JSON = "ro-crate-metadata.json"; + + protected static final String SPECIFICATION_PREFIX = + "https://w3id.org/ro/crate/"; + + protected static final String PROP_ABOUT = "about"; + protected static final String PROP_CONTEXT = "@context"; + protected static final String PROP_CONFORMS_TO = "conformsTo"; + protected static final String PROP_GRAPH = "@graph"; + protected static final String PROP_HAS_PART = "hasPart"; + protected static final String PROP_ID = "@id"; + + private final GenericReaderStrategy strategy; + + public CrateReader(GenericReaderStrategy strategy) { + this.strategy = strategy; + } + + /** + * This function will read the location (using one of the specified + * strategies) and then build the relation between the entities. + * + * @param location the location of the ro-crate to be read + * @return the read RO-crate + * + * @throws IOException if the crate cannot be read + */ + public RoCrate readCrate(T location) throws IOException { + // get the ro-crate-metadata.json + JsonObject metadataJson = strategy.readMetadataJson(location); + // get the content of the crate + File files = strategy.readContent(location); + + // this set will contain the files that are associated with entities + HashSet usedFiles = new HashSet<>(); + 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).markAsImported(); + } + + private RoCrate rebuildCrate( + JsonObject metadataJson, + File files, + HashSet usedFiles + ) { + Objects.requireNonNull( + metadataJson, + "metadataJson must not be null – did the strategy fail to locate 'ro-crate-metadata.json'?" + ); + Objects.requireNonNull( + files, + "files directory must not be null – check GenericReaderStrategy.readContent()" + ); + JsonValue context = metadataJson.get(PROP_CONTEXT); + + CrateMetadataContext crateContext = new RoCrateMetadataContext(context); + RoCrateUnsafe crate = new RoCrateUnsafe(); + crate.setMetadataContext(crateContext); + JsonValue graph = metadataJson.get(PROP_GRAPH); + + if (graph.isArray()) { + moveRootEntitiesFromGraphToCrate(crate, (JsonArray) graph); + } + RootDataEntity root = crate.getRootDataEntity(); + boolean rootExtractionSuccessful = root != null; + + if (rootExtractionSuccessful) { + Set dataEntityIds = getDataEntityIds(root, graph); + for (JsonValue entityJson : graph) { + String eId = unpackId(entityJson); + JsonObject properties = (JsonObject) entityJson.deepCopy(); + boolean isDataEntity = dataEntityIds.contains(eId); + + if (isDataEntity) { + DataEntity data = null; + boolean isDataSet = + properties.path("@type").asText().equals("Dataset") || + properties + .path("@type") + .valueStream() + .anyMatch(typeString -> typeString.asText().equals("Dataset")); + if (isDataSet) { + data = new DataSetEntity.DataSetBuilder() + .setAllUnsafe(properties) + .build(); + } else { + // data entity + DataEntity.DataEntityBuilder builder = + new DataEntity.DataEntityBuilder().setAllUnsafe(properties); + + // Handle data entities with corresponding file + checkFolderHasFile( + entityJson.get(PROP_ID).asText(), + files + ).ifPresent(file -> { + usedFiles.add(file.getPath()); + builder + .setLocationWithExceptions(file.toPath()) + .setId(file.getName()); + }); + data = builder.build(); + } + crate.addDataEntityWithoutRootHasPart(data); + } else { + // contextual entity + crate.addContextualEntity( + new ContextualEntity.ContextualEntityBuilder() + .setAllUnsafe(properties) + .build() + ); + } + } + } + + Collection untrackedFiles = Arrays.stream( + Optional.ofNullable(files.listFiles()).orElse(new File[0]) + ) + .filter(f -> !usedFiles.contains(f.getPath())) + .collect(Collectors.toSet()); + + crate.setUntrackedFiles(untrackedFiles); + Validator defaultValidation = new Validator(Validation.crateStrategy()); + defaultValidation.validate(crate); + return crate; + } + + /** + * Extracts graph connections from top to bottom. + *

+ * Example: (connections.get(parent) -> children) + * + * @param graph the JsonArray with all Entities. + * @return the graph connections. + */ + protected Map> makeEntityGraph(JsonValue graph) { + Map> connections = new HashMap<>(); + + Map idToNodes = new HashMap<>(); + StreamSupport.stream(graph.spliterator(), false).forEach(jsonNode -> + idToNodes.put(unpackId(jsonNode), jsonNode) + ); + + for (JsonValue entityNode : graph) { + String currentId = unpackId(entityNode); + StreamSupport.stream(entityNode.path("hasPart").spliterator(), false) + .map(this::unpackId) + .map(s -> idToNodes.getOrDefault(s, null)) + .filter(Objects::nonNull) + .forEach(child -> + connections + .computeIfAbsent(currentId, key -> new HashSet<>()) + .add(unpackId(child)) + ); + StreamSupport.stream(entityNode.path("isPartOf").spliterator(), false) + .map(this::unpackId) + .map(s -> idToNodes.getOrDefault(s, null)) + .filter(Objects::nonNull) + .forEach(parent -> + connections + .computeIfAbsent(unpackId(parent), key -> new HashSet<>()) + .add(currentId) + ); + } + return connections; + } + + protected Set getDataEntityIds(RootDataEntity root, JsonValue graph) { + if (root == null) { + return Set.of(); + } + Map> network = makeEntityGraph(graph); + Set directDataEntities = new HashSet<>(root.hasPart); + + Stack processingQueue = new Stack<>(); + processingQueue.addAll(directDataEntities); + Set result = new HashSet<>(); + + while (!processingQueue.empty()) { + String currentId = processingQueue.pop(); + result.add(currentId); + network + .getOrDefault(currentId, new HashSet<>()) + .stream() + .filter(subId -> !result.contains(subId)) // avoid loops! + .forEach(subId -> { + result.add(subId); + processingQueue.add(subId); + }); + } + return result; + } + + protected String unpackId(JsonValue node) { + if (node.isTextual()) { + return node.asText(); + } else /*if (node.isObject())*/ { + return node.path(PROP_ID).asText(); + } + } + + protected Optional checkFolderHasFile( + String filepathOrId, + File folder + ) { + if (IdentifierUtils.isUrl(filepathOrId)) { + return Optional.empty(); + } + return Stream.of( + IdentifierUtils.decode(filepathOrId).orElse(filepathOrId), + filepathOrId + ) + .map(filename -> + folder.toPath().resolve(filename).normalize().toAbsolutePath() + ) + // defence-in-depth: ensure we are still inside the crate folder + .filter(resolved -> resolved.startsWith(folder.toPath())) + .map(Path::toFile) + .filter(File::exists) + .findFirst(); + } + + /** + * Moves the descriptor and the root entity from the graph to the crate. + *

+ * Extracts the root data entity and the Metadata File Descriptor from the + * graph and inserts them into the crate object. It also deletes it from the + * graph. We will need the root dataset to distinguish between data entities + * and contextual entities. + * + * @param crate the crate, which will receive the entities, if available in + * the graph. + * @param graph the graph of the Metadata JSON file, where the entities are + * extracted and removed from. + */ + protected void moveRootEntitiesFromGraphToCrate( + RoCrate crate, + JsonArray graph + ) { + Optional maybeDescriptor = getMetadataDescriptor(graph); + + maybeDescriptor.ifPresent(descriptor -> { + setCrateDescriptor(crate, descriptor); + JsonUtilFunctions.removeJsonNodeFromArrayNode(graph, descriptor); + + Optional maybeRoot = extractRoot(graph, descriptor); + + maybeRoot.ifPresent(root -> { + Set hasPartIds = extractHasPartIds(root); + + crate.setRootDataEntity( + new RootDataEntity.RootDataEntityBuilder() + .setAllUnsafe(root.deepCopy()) + .setHasPart(hasPartIds) + .build() + ); + + JsonUtilFunctions.removeJsonNodeFromArrayNode(graph, root); + }); + }); + } + + /** + * Find the metadata descriptor. + *

+ * Currently prefers algorithm of version 1.1 over the one of 1.2-DRAFT. + * + * @param graph the graph to search the descriptor in. + * @return the metadata descriptor of the crate. + */ + protected Optional getMetadataDescriptor(JsonArray graph) { + boolean isParallel = graph.size() > PARALLELIZATION_THRESHOLD; + // use the algorithm described here: + // https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity + Optional maybeDescriptor = StreamSupport.stream( + graph.spliterator(), + isParallel + ) + // "2. if the conformsTo property is a URI that starts with + // https://w3id.org/ro/crate/" + .filter(node -> + node + .path(PROP_CONFORMS_TO) + .path(PROP_ID) + .asText() + .startsWith(SPECIFICATION_PREFIX) + ) + // "3. from this entity’s about object keep the @id URI as variable root" + .filter(node -> node.path(PROP_ABOUT).path(PROP_ID).isTextual()) + // There should be only one descriptor. If multiple exist, we take the first + // one. + .findFirst(); + return maybeDescriptor.or(() -> + StreamSupport.stream(graph.spliterator(), isParallel) + .filter(node -> node.path(PROP_ID).asText().equals(FILE_METADATA_JSON)) + // from https://www.researchobject.org/ro-crate/1.2-DRAFT/root-data-entity.html#finding-the-root-data-entity + .findFirst() + ); + } + + /** + * Extracts the root entity from the graph, using the information from the + * descriptor. + *

+ * Basically implements step 5 of the algorithm described here: + * + * https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity + * + * + * @param graph the graph from the metadata JSON-LD file + * @param descriptor the RO-Crate descriptor + * @return the root entity, if found + */ + private Optional extractRoot( + JsonArray graph, + JsonValue descriptor + ) { + String rootId = descriptor.get(PROP_ABOUT).get(PROP_ID).asText(); + boolean isParallel = graph.size() > PARALLELIZATION_THRESHOLD; + return StreamSupport.stream(graph.spliterator(), isParallel) + // root is an object (filter + conversion) + .filter(JsonValue::isObject) + .map(node -> (JsonObject) node.deepCopy()) + // "5. if the entity has an @id URI that matches root return it" + .filter(node -> node.path(PROP_ID).asText().equals(rootId)) + .findFirst(); + } + + private Set extractHasPartIds(JsonObject root) { + JsonValue hasPartNode = root.path(PROP_HAS_PART); + boolean isParallel = + hasPartNode.isArray() && hasPartNode.size() > PARALLELIZATION_THRESHOLD; + Set hasPartIds = StreamSupport.stream( + hasPartNode.spliterator(), + isParallel + ) + .map(hasPart -> hasPart.path(PROP_ID).asText()) + .filter(text -> !text.isBlank()) + .collect(Collectors.toSet()); + if (hasPartIds.isEmpty() && hasPartNode.path(PROP_ID).isTextual()) { + hasPartIds.add(hasPartNode.path(PROP_ID).asText()); + } + return hasPartIds; + } + + private void setCrateDescriptor(RoCrate crate, JsonValue descriptor) { + ContextualEntity descriptorEntity = + new ContextualEntity.ContextualEntityBuilder() + .setAllUnsafe((JsonObject) descriptor.deepCopy()) + .build(); + crate.setJsonDescriptor(descriptorEntity); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java new file mode 100644 index 00000000..d51d6c95 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java @@ -0,0 +1,29 @@ +package edu.kit.datamanager.ro_crate.reader; + +import edu.kit.datamanager.ro_crate.json.JsonObject; +import java.io.File; +import java.io.IOException; + +/** + * Generic interface for the strategy of the reader class. + * This allows for flexible input types when implementing different reading strategies. + * + * @param the type which determines the source of the crate + */ +public interface GenericReaderStrategy { + /** + * Read the metadata.json file from the given location. + * + * @param location the location to read from + * @return the parsed metadata.json as JsonObject + */ + JsonObject readMetadataJson(SOURCE_TYPE location) throws IOException; + + /** + * Read the content from the given location. + * + * @param location the location to read from + * @return the content as a File + */ + File readContent(SOURCE_TYPE location) throws IOException; +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java new file mode 100644 index 00000000..1ceaae48 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java @@ -0,0 +1,31 @@ +package edu.kit.datamanager.ro_crate.reader; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +/** + * A class for reading a crate from a folder. + * + * @author Nikola Tzotchev on 9.2.2022 г. + * @version 1 + */ +public class ReadFolderStrategy implements GenericReaderStrategy { + + @Override + public JsonObject readMetadataJson(String location) throws IOException { + Path metadata = new File(location) + .toPath() + .resolve("ro-crate-metadata.json"); + JsonObject objectNode = Json.objectNode(); + objectNode = (JsonObject) Json.parse(metadata.toFile()).deepCopy(); + return objectNode; + } + + @Override + public File readContent(String location) { + return new File(location); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java similarity index 83% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java index 5a2474b5..94e1711a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java @@ -1,18 +1,16 @@ package edu.kit.datamanager.ro_crate.reader; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; import edu.kit.datamanager.ro_crate.util.FileSystemUtil; -import net.lingala.zip4j.ZipFile; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.FileFilterUtils; - import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.UUID; +import net.lingala.zip4j.ZipFile; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.FileFilterUtils; /** * Reads a crate from a ZIP archive (file). @@ -42,7 +40,9 @@ public class ReadZipStrategy implements GenericReaderStrategy { protected final String ID = UUID.randomUUID().toString(); - protected Path temporaryFolder = Path.of(String.format("./.tmp/ro-crate-java/zipReader/%s/", ID)); + protected Path temporaryFolder = Path.of( + String.format("./.tmp/ro-crate-java/zipReader/%s/", ID) + ); protected boolean isExtracted = false; /** @@ -106,29 +106,41 @@ private void readCrate(String location) throws IOException { } @Override - public ObjectNode readMetadataJson(String location) throws IOException { + public JsonObject readMetadataJson(String location) throws IOException { if (!isExtracted) { this.readCrate(location); } - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - File jsonMetadata = this.temporaryFolder.resolve(JsonDescriptor.ID).toFile(); + File jsonMetadata = this.temporaryFolder + .resolve(JsonDescriptor.ID) + .toFile(); if (!jsonMetadata.isFile()) { // Try to find the metadata file in subdirectories File firstSubdir = FileUtils.listFilesAndDirs( - temporaryFolder.toFile(), - FileFilterUtils.directoryFileFilter(), - null // not recursive - ) + temporaryFolder.toFile(), + FileFilterUtils.directoryFileFilter(), + null // not recursive + ) .stream() .limit(50) - .filter(file -> file.toPath().toAbsolutePath().resolve(JsonDescriptor.ID).toFile().isFile()) + .filter(file -> + file + .toPath() + .toAbsolutePath() + .resolve(JsonDescriptor.ID) + .toFile() + .isFile() + ) .findFirst() - .orElseThrow(() -> new IllegalStateException("No %s found in zip file".formatted(JsonDescriptor.ID))); + .orElseThrow(() -> + new IllegalStateException( + "No %s found in zip file".formatted(JsonDescriptor.ID) + ) + ); jsonMetadata = firstSubdir.toPath().resolve(JsonDescriptor.ID).toFile(); } - return objectMapper.readTree(jsonMetadata).deepCopy(); + return (JsonObject) Json.parse(jsonMetadata).deepCopy(); } @Override diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java new file mode 100644 index 00000000..26a748ff --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java @@ -0,0 +1,192 @@ +package edu.kit.datamanager.ro_crate.reader; + +import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.util.FileSystemUtil; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.UUID; +import net.lingala.zip4j.io.inputstream.ZipInputStream; +import net.lingala.zip4j.model.LocalFileHeader; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Reads a crate from a streamed ZIP archive. + *

+ * This class handles reading and extraction of RO-Crate content from ZIP archives + * into a temporary directory structure on the file system, + * which allows accessing the contained files. + *

+ * Supports ELN-Style crates, + * meaning the crate may be either in the zip archive directly or in a single, + * direct subfolder beneath the root folder (/folder). + *

+ * Note: This implementation checks for up to 50 subdirectories if multiple are present. + * This is to avoid zip bombs, which may contain a lot of subdirectories, + * and at the same time gracefully handle valid crated with hidden subdirectories + * (for example, thumbnails). + *

+ * NOTE: The resulting crate may refer to these temporary files. Therefore, + * these files are only being deleted before the JVM exits. If you need to free + * space because your application is long-running or creates a lot of + * crates, you may use the getters to retrieve information which will help + * you to clean up manually. Keep in mind that crates may refer to this + * folder after extraction. Use RoCrateWriter to export it so some + * persistent location and possibly read it from there, if required. Or use + * the ZipWriter to write it back to its source. + * + * @author jejkal + */ +public class ReadZipStreamStrategy + implements GenericReaderStrategy +{ + + private static final Logger logger = LoggerFactory.getLogger( + ReadZipStreamStrategy.class + ); + protected final String ID = UUID.randomUUID().toString(); + protected Path temporaryFolder = Path.of( + String.format("./.tmp/ro-crate-java/zipStreamReader/%s/", ID) + ); + protected boolean isExtracted = false; + + /** + * Crates an instance with the default configuration. + *

+ * The default configuration is to extract the ZipFile to + * `./.tmp/ro-crate-java/zipStreamReader/%UUID/`. + */ + public ReadZipStreamStrategy() {} + + /** + * Creates a ZipStreamReader which will extract the contents temporary to + * the given location instead of the default location. + * + * @param folderPath the custom directory to extract content to for + * temporary access. + * @param shallAddUuidSubfolder if true, the reader will extract into + * subdirectories of the given directory. These subdirectories will have + * UUIDs as their names. + */ + public ReadZipStreamStrategy(Path folderPath, boolean shallAddUuidSubfolder) { + if (shallAddUuidSubfolder) { + this.temporaryFolder = folderPath.resolve(ID); + } else { + this.temporaryFolder = folderPath; + } + } + + /** + * @return the identifier which may be used as the name for a subfolder in + * the temporary directory. + */ + public String getID() { + return ID; + } + + /** + * @return the folder (considered temporary) where the zipped crate will be + * or has been extracted to. + */ + public Path getTemporaryFolder() { + return temporaryFolder; + } + + /** + * @return whether the crate has already been extracted into the temporary + * folder. + */ + public boolean isExtracted() { + return isExtracted; + } + + /**Read the crate metadata and content from the provided input stream. + * + * @param stream The input stream. + */ + private void readCrate(InputStream stream) throws IOException { + File folder = temporaryFolder.toFile(); + FileSystemUtil.mkdirOrDeleteContent(folder); + + LocalFileHeader localFileHeader; + int readLen; + byte[] readBuffer = new byte[4096]; + + try (ZipInputStream zipInputStream = new ZipInputStream(stream)) { + while ((localFileHeader = zipInputStream.getNextEntry()) != null) { + String fileName = localFileHeader.getFileName(); + File extractedFile = new File(folder, fileName).getCanonicalFile(); + Path targetRoot = folder.toPath().toRealPath(); + if (!extractedFile.toPath().startsWith(targetRoot)) { + throw new IOException( + "Entry is outside of target directory: " + fileName + ); + } + if (localFileHeader.isDirectory()) { + FileUtils.forceMkdir(extractedFile); + continue; + } + FileUtils.forceMkdir(extractedFile.getParentFile()); + try (OutputStream outputStream = new FileOutputStream(extractedFile)) { + while ((readLen = zipInputStream.read(readBuffer)) != -1) { + outputStream.write(readBuffer, 0, readLen); + } + } + } + } + this.isExtracted = true; + // register deletion on exit + FileUtils.forceDeleteOnExit(folder); + } + + @Override + public JsonObject readMetadataJson(InputStream stream) throws IOException { + if (!isExtracted) { + this.readCrate(stream); + } + + File jsonMetadata = temporaryFolder.resolve(JsonDescriptor.ID).toFile(); + if (!jsonMetadata.isFile()) { + // Try to find the metadata file in subdirectories + File firstSubdir = FileUtils.listFilesAndDirs( + temporaryFolder.toFile(), + FileFilterUtils.directoryFileFilter(), + null + ) + .stream() + .limit(50) + .filter(file -> + file + .toPath() + .toAbsolutePath() + .resolve(JsonDescriptor.ID) + .toFile() + .isFile() + ) + .findFirst() + .orElseThrow(() -> + new IllegalStateException( + "No %s found in zip file".formatted(JsonDescriptor.ID) + ) + ); + jsonMetadata = firstSubdir.toPath().resolve(JsonDescriptor.ID).toFile(); + } + return (JsonObject) Json.parse(jsonMetadata).deepCopy(); + } + + @Override + public File readContent(InputStream stream) throws IOException { + if (!isExtracted) { + this.readCrate(stream); + } + return temporaryFolder.toFile(); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java new file mode 100644 index 00000000..8dc6436e --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java @@ -0,0 +1,87 @@ +package edu.kit.datamanager.ro_crate.reader; + +import java.io.InputStream; +import java.nio.file.Path; + +/** + * Factory for creating common RO-Crate reader instances. + * Provides convenient static methods to instantiate readers with pre-configured strategies. + */ +public class Readers { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Readers() {} + + /** + * Creates a reader that reads from ZIP files using input streams. + * + * @return A reader configured for ZIP files + * + * @see ReadZipStreamStrategy#ReadZipStreamStrategy() + */ + public static CrateReader newZipStreamReader() { + return new CrateReader<>(new ReadZipStreamStrategy()); + } + + /** + * Creates a reader that reads from ZIP files using input streams, + * extracting to a custom temporary location. + * + * @param extractPath Path where ZIP contents should be extracted + * @param useUuidSubfolder Whether to create a UUID subfolder under extractPath + * @return A reader configured for ZIP files with custom extraction + * + * @see ReadZipStreamStrategy#ReadZipStreamStrategy(Path, boolean) + */ + public static CrateReader newZipStreamReader( + Path extractPath, + boolean useUuidSubfolder + ) { + return new CrateReader<>( + new ReadZipStreamStrategy(extractPath, useUuidSubfolder) + ); + } + + /** + * Creates a reader that reads from a folder using a string path. + * + * @return A reader configured for folders + * + * @see ReadFolderStrategy + */ + public static CrateReader newFolderReader() { + return new CrateReader<>(new ReadFolderStrategy()); + } + + /** + * Creates a reader that reads from a ZIP file using a string path. + * + * @return A reader configured for ZIP files + * + * @see ReadZipStrategy#ReadZipStrategy() + */ + public static CrateReader newZipPathReader() { + return new CrateReader<>(new ReadZipStrategy()); + } + + /** + * Creates a reader that reads from a ZIP file using a string path, + * extracting to a custom temporary location. + * + * @param extractPath Path where ZIP contents should be extracted + * @param useUuidSubfolder Whether to create a UUID subfolder under extractPath + * @return A reader configured for ZIP files with custom extraction + * + * @see ReadZipStrategy#ReadZipStrategy(Path, boolean) + */ + public static CrateReader newZipPathReader( + Path extractPath, + boolean useUuidSubfolder + ) { + return new CrateReader<>( + new ReadZipStrategy(extractPath, useUuidSubfolder) + ); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java similarity index 86% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java index d11177c1..9156b9ba 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java @@ -14,7 +14,8 @@ */ @Deprecated(since = "2.1.0", forRemoval = true) public class RoCrateReader extends CrateReader { - public RoCrateReader(GenericReaderStrategy reader) { - super(reader); - } + + public RoCrateReader(GenericReaderStrategy reader) { + super(reader); + } } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java b/core/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java new file mode 100644 index 00000000..4e7bf749 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java @@ -0,0 +1,111 @@ +package edu.kit.datamanager.ro_crate.special; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +/** + * Represents a crate version and has information for each version available. + * + * It offers also convenience functionality like version comparison similar to + * numbers. + */ +public enum CrateVersion { + // NOTE: Java defines compareTo for enums to work in the order of the defined + // elements. This means higher versions have to be defined below the lower + // versions. Consider this when adding a new version. + // + // NOTE: To add a new version, simply append a new variant with the correct + // data and read the comment below the versions. + V1P1("https://w3id.org/ro/crate/1.1"), + V1P2_DRAFT("https://w3id.org/ro/crate/1.2-DRAFT"); + + // NOTE: Do not forget to adjust the following when adding a version: + public static final CrateVersion LATEST_STABLE = CrateVersion.V1P1; + public static final CrateVersion LATEST_UNSTABLE = CrateVersion.V1P2_DRAFT; + + /** + * The String which is referred to by the crates conformsTo value. + * Should lead to the specification when resolved as a URL. + */ + public final String conformsTo; + + /** + * The String representation of the version. + * + * Example: assertEquals("1.2-DRAFT", CrateVersion.V1P2_DRAFT); + */ + public final String version; + + /** + * Basically a constructor of a version, if it needs to be created from the spec + * URI. + * + * @param conformsTo the specification URI. Example: + * https://w3id.org/ro/crate/1.1 + * @return the matching CrateVersion enum, if the URI matches any. Empty if not. + */ + public static Optional fromSpecUri(String conformsTo) { + return Optional.ofNullable(crateVersionOfConformsTo(conformsTo)); + } + + /** + * Private constructor which is being used for the internally given information + * above. + * + * @param spec the specification URI / conformsTo value + */ + private CrateVersion(String spec) { + this.conformsTo = spec; + this.version = getVersionFromConformsToString(spec); + } + + public boolean isStable() { + return !this.version.endsWith("-DRAFT"); + } + + public URI getConformsToUri() { + try { + return new URI(this.conformsTo); + } catch (URISyntaxException e) { + // can not happen as the user can not set this string + return null; + } + } + + /** + * Extracts the version from the spec uri. + * + * @param specUri the uri of the spec + * @return the version of the spec + */ + private static String getVersionFromConformsToString(String specUri) { + String[] parts = specUri.split("/"); + return parts[parts.length - 1]; + } + + private static CrateVersion crateVersionOfConformsTo(String conformsTo) { + for (CrateVersion e : values()) { + if (e.conformsTo.equals(conformsTo)) { + return e; + } + } + return null; + } + + public boolean isGreaterThan(CrateVersion other) { + return this.compareTo(other) > 0; + } + + public boolean isGreaterOrEqualThan(CrateVersion other) { + return this.compareTo(other) >= 0; + } + + public boolean isLowerThan(CrateVersion other) { + return this.compareTo(other) < 0; + } + + public boolean isLowerOrEqualThan(CrateVersion other) { + return this.compareTo(other) <= 0; + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java b/core/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java new file mode 100644 index 00000000..d8cebdfc --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java @@ -0,0 +1,161 @@ +package edu.kit.datamanager.ro_crate.special; + +import com.apicatalog.jsonld.uri.UriUtils; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Optional; + +/** + * This class defines methods regarding URIs in general, which in RO-Crate + * context means usually a valid, resolvable URL or a relative file path. + *

+ * The purpose is to have a simple abstraction where the way e.g. a URL is + * checked can be changed and tested easily for the whole library. + */ +public class IdentifierUtils { + + /** Static class */ + private IdentifierUtils() {} + + /** + * Returns true, if the given String is encoded and can be used as an + * identifier in RO-Crate. + *

+ * Possible identifiers include: + * - a uri + * - a url + * - a valid LD identifier (including blank node identifiers) + * - relative identifiers + * + * @param uri the given URI. Usually a URL or relative file path. + * @return true if the url is encoded, false if it is not. + */ + public static boolean isValidUri(String uri) { + return UriUtils.isURI(uri) || isLdBlankNode(uri); + } + + /** + * Returns true, if the given string is a url. + * + * @param uri the given string + * @return true if it is a url, false otherwise. + */ + public static boolean isUrl(String uri) { + try { + return encode(uri) + .map(encodedUri -> asUrl(encodedUri).isPresent()) + .orElse(false); + } catch (Exception e) { + return false; + } + } + + /** + * Tests if the given String is a URL, and if so, returns it. + * + * @param uriAsString the given String which will be tested. + * @return the url, if it is one. + */ + public static Optional asUrl(String uriAsString) { + try { + URI uri = new URI(uriAsString); + if (uri.isAbsolute()) { + return Optional.of(uri.toURL()); + } + } catch ( + URISyntaxException + | MalformedURLException + | NullPointerException ex + ) { + return Optional.empty(); + } + return Optional.empty(); + } + + /** + * Returns true, if the given string is a file path. + * + * @param uri the given string + * @return true if it is a path, false otherwise. + */ + public static boolean isPath(String uri) { + return asPath(uri).isPresent(); + } + + /** + * Tests if the given String is a file path, and if so, returns it. + * + * @param uri the given String which will be tested. + * @return the path, if it is one. + */ + public static Optional asPath(String uri) { + try { + Path u = Path.of(uri); + if (!isUrl(uri)) { + return Optional.of(u); + } + } catch (InvalidPathException e) { + return Optional.empty(); + } + return Optional.empty(); + } + + /** + * Encodes a string using the description if the ro-crate-specification. + * + * @param uri the string to encode. + * @return the encoded version of the given string, if possible. + */ + public static Optional encode(String uri) { + String result = uri; + // according to + // https://www.researchobject.org/ro-crate/1.1/data-entities.html#encoding-file-paths + // a file path may not be fully encoded and may contain international unicode + // characters. So we try a "soft"-encoding first, and if this is not yet valid, + // we really fully encode it. + + result = result.replace("\\", "/"); // "...express any relative paths using / separator..." + result = result.replace("%", "%25"); // "...escape special characters like space (%25)..." + result = result.replace(" ", "%20"); // "...and percent (%25)..." + if (isValidUri(result)) { + return Optional.of(result); + } + // from here on, our soft-encoding failed and we will fully encode the url or + // path. + result = URLEncoder.encode(uri, StandardCharsets.UTF_8); + result = result.replace("+", "%20"); // UrlEncoder encodes spaces with "+" + result = result.replace("%3A", ":"); // Convert colons back + result = result.replace("%2F", "/"); // Convert slashes back + + if (isValidUri(result)) { + return Optional.of(result); + } else { + return Optional.empty(); + } + } + + public static Optional decode(String uri) { + if (!isValidUri(uri) || isLdBlankNode(uri)) { + return Optional.of(uri); + } + return Optional.of(URLDecoder.decode(uri, StandardCharsets.UTF_8)); + } + + /** + * Returns true if the given string is a blank node identifier in the Linked + * Data world. + * + * @param uri the given String + * @return true if the string is a blank node. False if not. + */ + private static boolean isLdBlankNode(String uri) { + return uri.startsWith("_:") && !uri.contains(" "); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java b/core/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java similarity index 75% rename from src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java index 1503d87f..bc75557e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java @@ -1,10 +1,10 @@ package edu.kit.datamanager.ro_crate.special; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; import java.util.HashSet; import java.util.Set; @@ -20,8 +20,8 @@ public class JsonUtilFunctions { * @param node the json node that should be unwrapped. * @return the unwrapped Json. */ - public static JsonNode unwrapSingleArray(JsonNode node) { - ObjectNode newNode = MyObjectMapper.getMapper().createObjectNode(); + public static JsonValue unwrapSingleArray(JsonValue node) { + JsonObject newNode = Json.objectNode(); if (node.isObject()) { var itr = node.fields(); while (itr.hasNext()) { @@ -33,7 +33,7 @@ public static JsonNode unwrapSingleArray(JsonNode node) { if (node.size() == 1) { return unwrapSingleArray(node.get(0)); } else { - ArrayNode arrayNode = MyObjectMapper.getMapper().createArrayNode(); + JsonArray arrayNode = Json.arrayNode(); for (var n : node) { arrayNode.add(unwrapSingleArray(n)); } @@ -49,10 +49,10 @@ public static JsonNode unwrapSingleArray(JsonNode node) { * @param id every property with this as id will be removed. * @param node the json from which to remove the properties. */ - public static void removeFieldsWith(String id, JsonNode node) { - //MyObjectMapper.getMapper().createObjectNode(); + public static void removeFieldsWith(String id, JsonValue node) { + //Json.objectNode(); if (node.isObject()) { - ObjectNode newNode = (ObjectNode) node; + JsonObject newNode = (JsonObject) node; var itr = newNode.fields(); while (itr.hasNext()) { var nxt = itr.next(); @@ -65,7 +65,7 @@ public static void removeFieldsWith(String id, JsonNode node) { } } } else if (node.isArray()) { - ArrayNode arrayNode = (ArrayNode) node; + JsonArray arrayNode = (JsonArray) node; for (int i = 0; i < arrayNode.size(); i++) { var p = arrayNode.get(i); if (p.isValueNode()) { @@ -83,10 +83,10 @@ public static void removeFieldsWith(String id, JsonNode node) { * This method extracts from every property of a json objects its id's. * The method is intended for flattened json objects. * - * @param node The JsonNode json object. + * @param node The JsonValue json object. * @return the set containing all the strings. */ - public static Set getIdPropertiesFromJsonNode(JsonNode node) { + public static Set getIdPropertiesFromJsonNode(JsonValue node) { Set set = new HashSet<>(); var itr = node.fields(); while (itr.hasNext()) { @@ -96,14 +96,14 @@ public static Set getIdPropertiesFromJsonNode(JsonNode node) { } /** - * Extracts the id from a JsonNode property. + * Extracts the id from a JsonValue property. * This method is intended for flattened json-ld * that is why it is not recursive * - * @param node the JsonNode property + * @param node the JsonValue property * @return Set containing all the id as string */ - public static Set getIdPropertiesFromProperty(JsonNode node) { + public static Set getIdPropertiesFromProperty(JsonValue node) { Set set = new HashSet<>(); if (node.isArray()) { for (var element : node) { @@ -131,7 +131,7 @@ public static Set getIdPropertiesFromProperty(JsonNode node) { * @return true if an equal node was found and removed, false if no equal node * was found. */ - public static boolean removeJsonNodeFromArrayNode(ArrayNode array, JsonNode node) { + public static boolean removeJsonNodeFromArrayNode(JsonArray array, JsonValue node) { for (int i = 0; i < array.size(); i++) { if (array.get(i).equals(node)) { array.remove(i); diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java new file mode 100644 index 00000000..8431ee44 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -0,0 +1,47 @@ +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 { + + public static final String VERSION_PROPERTIES = "version.properties"; + + /** + * 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() { + // Lazy initialization - version loaded on first access + } + + @Override + public String getVersion() { + if (cachedVersion != null) { + return cachedVersion; + } + + 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; + return version.trim(); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to read version from properties file", + e + ); + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java new file mode 100644 index 00000000..2a2a86f8 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java @@ -0,0 +1,128 @@ +package edu.kit.datamanager.ro_crate.util; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.regex.Matcher; +import org.apache.commons.io.FileUtils; + +public class FileSystemUtil { + + private FileSystemUtil() { + // Utility class, no instantiation + } + + /** + * Checks if the given ID appears to be a file path. + *

+ * Specifically excludes IDs starting with "doi:", "http", or "https". + * + * @param id the ID to check + * @return true if it looks like a file path, false otherwise + */ + public static boolean isFilePath(String id) { + return ( + id != null && + !( + id.startsWith("doi:") || + id.startsWith("http://") || + id.startsWith("https://") + ) + ); + } + + /** + * Gets the parent path of a given path. + * @param path the path to evaluate. + * @return the parent path, or null if no parent exists. + */ + public static String getParentPath(String path) { + if (path == null || path.equals("./") || path.isEmpty()) { + return null; + } + + // Normalize path - remove trailing slash for consistency + String normalizedPath = path.endsWith("/") + ? path.substring(0, path.length() - 1) + : path; + + int lastSlash = normalizedPath.lastIndexOf('/'); + if (lastSlash == -1) { + return "./"; // Root directory + } + + String parentPath = normalizedPath.substring(0, lastSlash); + + // If parent is empty, it's root + if (parentPath.isEmpty()) { + return "./"; + } + + // For validation, we need to check both with and without trailing slash + // since files don't have trailing slash but folders do + return parentPath; + } + + /** + * Removes a specific set of given file extensions from a file name, if present. + * The extensions are case-insensitive. Given "ELN", "eln" or "Eln" will also match. + * The dot (.) before the extension is also assumed and removed implicitly: + *

+ * Example: + * filterExtensionsFromFileName("test.eln", Set.of("ELN")) -> "test" + * + * @param filename the file name to filter (must not be null) + * @param extensionsToRemove the extensions to remove (must not be null) + * @return the filtered file name + * @throws NullPointerException if any parameter is null + */ + public static String filterExtensionsFromFileName( + String filename, + Collection extensionsToRemove + ) { + String dot = Matcher.quoteReplacement("."); + String end = Matcher.quoteReplacement("$"); + for (String extension : extensionsToRemove) { + // (?i) removes case sensitivity + filename = filename.replaceFirst("(?i)" + dot + extension + end, ""); + } + return filename; + } + + /** + * Ensures that a given path ends with a trailing slash. + * + * @param path the path to check + * @return the path with a trailing slash if it didn't have one, or the original path + */ + public static String ensureTrailingSlash(String path) { + if (path == null || path.isEmpty()) { + return path; + } + if (!path.endsWith("/")) { + return path + "/"; + } + return path; + } + + /** + * Creates a directory or deletes its content if it already exists. + * + * @param folder the folder to create or delete content from + * @throws IOException if an I/O error occurs + */ + public static void mkdirOrDeleteContent(File folder) throws IOException { + File[] files = folder.listFiles(); + boolean isNonEmptyDir = + folder.exists() && + folder.isDirectory() && + files != null && + files.length > 0; + boolean isFile = folder.exists() && !folder.isDirectory(); + + if (isNonEmptyDir || isFile) { + FileUtils.forceDelete(folder); + } + FileUtils.forceMkdir(folder); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java new file mode 100644 index 00000000..7971b85e --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -0,0 +1,60 @@ +package edu.kit.datamanager.ro_crate.util; + +import edu.kit.datamanager.ro_crate.json.JsonValue; +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 { + + private Graph() { + // Private constructor to prevent instantiation + } + + /** + * 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 JsonValue if found, null otherwise. + */ + public static JsonValue findEntityById(JsonValue graph, String id) { + for (JsonValue 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 JsonValue if found, null otherwise. + */ + public static JsonValue findEntityByType(JsonValue 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 JsonValue containing all entities of the specified type. + */ + public static JsonValue[] findEntitiesByType(JsonValue graph, String type) { + return StreamSupport.stream(graph.spliterator(), false) + .filter(entity -> entity.path("@type").asText().equals(type)) + .toArray(JsonValue[]::new); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java new file mode 100644 index 00000000..e14dd222 --- /dev/null +++ b/core/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/core/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java new file mode 100644 index 00000000..e258b559 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java @@ -0,0 +1,100 @@ +package edu.kit.datamanager.ro_crate.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import net.lingala.zip4j.model.ZipParameters; + +/** + * + * @author jejkal + */ +public class ZipStreamUtil { + + /** + * Adds a folder and its contents to a ZipOutputStream. + * + * @param zipOutputStream The ZipOutputStream to which the folder will be added. + * @param folder The folder to be added. + * @param parentPath The path in the zip file where the folder will be added. + * @throws IOException If an I/O error occurs. + */ + public static void addFolderToZipStream( + ZipOutputStream zipOutputStream, + File folder, + String parentPath + ) throws IOException { + if (!folder.exists() || !folder.isDirectory()) { + throw new IllegalArgumentException( + "The provided folder path is not a valid directory: %s".formatted( + folder.getAbsolutePath() + ) + ); + } + + File[] files = folder.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + String zipEntryPath = parentPath.isEmpty() + ? file.getName() + : parentPath + "/" + file.getName(); + if (file.isDirectory()) { + addFolderToZipStream( + zipOutputStream, + file.getAbsolutePath(), + zipEntryPath + ); + } else { + addFileToZipStream(zipOutputStream, file, zipEntryPath); + } + } + } + + /** + * Adds a folder and its contents to a ZipOutputStream. + * @param zipOutputStream The ZipOutputStream to which the folder will be added. + * @param folderPath The path of the folder to be added. + * @param parentPath The path in the zip file where the folder will be added. + * @throws IOException If an I/O error occurs. + */ + public static void addFolderToZipStream( + ZipOutputStream zipOutputStream, + String folderPath, + String parentPath + ) throws IOException { + addFolderToZipStream(zipOutputStream, new File(folderPath), parentPath); + } + + /** + * Adds a file to a ZipOutputStream. + * + * @param zipOutputStream The ZipOutputStream to which the file will be added. + * @param file The file to be added. + * @param zipEntryPath The path in the zip file where the file will be added. + * @throws IOException If an I/O error occurs. + */ + public static void addFileToZipStream( + ZipOutputStream zipOutputStream, + File file, + String zipEntryPath + ) throws IOException { + ZipParameters zipParameters = new ZipParameters(); + zipParameters.setFileNameInZip(zipEntryPath); + zipOutputStream.putNextEntry(zipParameters); + + try (InputStream inputStream = new FileInputStream(file)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + zipOutputStream.write(buffer, 0, len); + } + } + + zipOutputStream.closeEntry(); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validation.java b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validation.java new file mode 100644 index 00000000..2dca8024 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validation.java @@ -0,0 +1,81 @@ +package edu.kit.datamanager.ro_crate.validation; + +import edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Access point for the JSON-schema validation strategies. + * + *

The concrete implementations live in the JSON backend modules + * (ro-crate-java-jackson2 or ro-crate-java-jackson3) and are discovered via + * {@link ServiceLoader}; exactly one backend must be on the classpath. This + * keeps the core free of any concrete JSON implementation. + */ +public final class Validation { + + private static volatile ValidatorStrategy crateStrategy; + private static volatile EntityValidationStrategy entityStrategy; + + private Validation() {} + + /** + * @return the crate-level JSON-schema validation of the active JSON backend + */ + public static ValidatorStrategy crateStrategy() { + ValidatorStrategy result = crateStrategy; + if (result == null) { + result = discover(ValidatorStrategy.class); + crateStrategy = result; + } + return result; + } + + /** + * @return the entity-level JSON-schema validation of the active JSON backend + */ + public static EntityValidationStrategy entityStrategy() { + EntityValidationStrategy result = entityStrategy; + if (result == null) { + result = discover(EntityValidationStrategy.class); + entityStrategy = result; + } + return result; + } + + private static T discover(Class type) { + return uniqueCandidate(ServiceLoader.load(type).iterator(), type); + } + + /** + * Returns the single candidate or throws an exception that names the + * backend artifacts to add or exclude, so users who do not know the module + * layout can fix the classpath themselves. + */ + static T uniqueCandidate(Iterator candidates, Class type) { + if (!candidates.hasNext()) { + throw new IllegalStateException( + "No " + + type.getSimpleName() + + " found on the classpath. Add a JSON " + + "backend as a dependency: edu.kit.datamanager:ro-crate-java " + + "(Jackson 2 backend) or edu.kit.datamanager:ro-crate-java-jackson3 " + + "(Jackson 3 backend)." + ); + } + T discovered = candidates.next(); + if (candidates.hasNext()) { + throw new IllegalStateException( + "Multiple " + + type.getSimpleName() + + " implementations found on the " + + "classpath; ro-crate-java supports only one JSON backend at a " + + "time. Keep the backend matching your Jackson version and exclude " + + "the other one, e.g. exclude edu.kit.datamanager:ro-crate-java " + + "(Jackson 2 backend) and add " + + "edu.kit.datamanager:ro-crate-java-jackson3 (Jackson 3 backend)." + ); + } + return discovered; + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java new file mode 100644 index 00000000..3aa67f7c --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -0,0 +1,57 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.validation.Validation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import java.io.IOException; + +/** + * The class used for writing (exporting) crates. The class uses a strategy + * pattern for writing crates as different formats. (zip, folders, etc.) + * + * @param the type which determines the destination of the result + */ +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; + } + + /** + * 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; + } + + /** + * This method saves the crate to a destination provided. + * + * @param crate the crate to write. + * @param destination the location where the crate should be written. + */ + public void save(Crate crate, DESTINATION_TYPE destination) + throws IOException { + Validator defaultValidation = new Validator(Validation.crateStrategy()); + defaultValidation.validate(crate); + if (this.provenanceManager != null) { + this.provenanceManager.addProvenanceInformation(crate); + } + this.strategy.save(crate, destination); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java new file mode 100644 index 00000000..bb767d1e --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java @@ -0,0 +1,27 @@ +package edu.kit.datamanager.ro_crate.writer; + +/** + * An Interface for {@link GenericWriterStrategy} implementations which support writing + * ELN-Style crates. + * + * @param the type which determines the destination of the result + */ +public interface ElnFormatWriter extends + GenericWriterStrategy { + /** + * Write in ELN format style, meaning with a root subfolder in the zip file. + * Same as {@link #withRootSubdirectory()}. + * + * @return this writer + */ + ElnFormatWriter usingElnStyle(); + + /** + * Alias with more generic name for {@link #usingElnStyle()}. + * + * @return this writer + */ + default ElnFormatWriter withRootSubdirectory() { + return this.usingElnStyle(); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java similarity index 59% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java index d06301a0..b0bed7bb 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java @@ -1,7 +1,6 @@ package edu.kit.datamanager.ro_crate.writer; import edu.kit.datamanager.ro_crate.Crate; - import java.io.IOException; /** @@ -11,11 +10,11 @@ * @param the type of the destination parameter */ public interface GenericWriterStrategy { - /** - * Saves the given crate to the specified destination. - * - * @param crate The crate to save - * @param destination The destination where the crate should be saved - */ - void save(Crate crate, DESTINATION_TYPE destination) throws IOException; + /** + * Saves the given crate to the specified destination. + * + * @param crate The crate to save + * @param destination The destination where the crate should be saved + */ + void save(Crate crate, DESTINATION_TYPE destination) throws IOException; } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java new file mode 100644 index 00000000..d6ef92bf --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -0,0 +1,165 @@ +package edu.kit.datamanager.ro_crate.writer; + +import static edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity.ContextualEntityBuilder; + +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.time.Instant; + +/** + * Manages provenance information for RO-Crates. + * 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; + } + + @Override + public String toString() { + return prefix; + } + } + + /** + * 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; + + /** + * 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; + } + + /** + * 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() + ); + } + + /** + * 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()) + ) && !crate.isImported(); + + String libraryId = this.getLibraryId(); + + // Create action entity first + ContextualEntity actionEntity = buildNewActionEntity( + isFirstWrite, + libraryId + ); + + // Create or update ro-crate-java entity + ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity( + crate, + actionEntity.getId(), + libraryId + ); + + // Add entities to crate + crate.addContextualEntity(roCrateJavaEntity); + crate.addContextualEntity(actionEntity); + } + + protected ContextualEntity buildNewActionEntity( + boolean isFirstWrite, + String libraryId + ) { + return new ContextualEntityBuilder() + .addType(isFirstWrite ? "CreateAction" : "UpdateAction") + .addIdProperty("result", "./") + .addProperty("startTime", Instant.now().toString()) + .addIdProperty("agent", libraryId) + .build(); + } + + protected ContextualEntity buildRoCrateJavaEntity( + Crate crate, + String newActionId, + String libraryId + ) { + String version = this.versionProvider.getVersion(); + ContextualEntity self = crate + .getAllContextualEntities() + .stream() + .filter(contextualEntity -> libraryId.equals(contextualEntity.getId())) + .findFirst() + .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/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java similarity index 79% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java index 9337d400..92d34af9 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java @@ -9,7 +9,7 @@ @Deprecated(since = "2.1.0", forRemoval = true) public class RoCrateWriter extends CrateWriter { - public RoCrateWriter(GenericWriterStrategy writer) { - super(writer); - } + public RoCrateWriter(GenericWriterStrategy writer) { + super(writer); + } } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java new file mode 100644 index 00000000..a15c6627 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java @@ -0,0 +1,100 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A class for writing a crate to a folder. + * + * @author Nikola Tzotchev on 9.2.2022 г. + * @version 1 + */ +public class WriteFolderStrategy implements GenericWriterStrategy { + + private static final Logger logger = LoggerFactory.getLogger( + WriteFolderStrategy.class + ); + + protected boolean writePreview = true; + + /** + * For internal use. Skips the preview generation when writing the crate. + * + * @return this instance of WriteFolderStrategy + * + * @deprecated May be removed in future versions. Not intended for public use. + */ + @Deprecated(since = "2.1.0", forRemoval = true) + public WriteFolderStrategy disablePreview() { + this.writePreview = false; + return this; + } + + @Override + public void save(Crate crate, String destination) throws IOException { + File file = new File(destination); + FileUtils.forceMkdir(file); + JsonValue node = Json.parse(crate.getJsonMetadata()); + String str = Json.writePretty(node); + InputStream inputStream = new ByteArrayInputStream( + str.getBytes(StandardCharsets.UTF_8) + ); + + File json = new File(destination, "ro-crate-metadata.json"); + FileUtils.copyInputStreamToFile(inputStream, json); + inputStream.close(); + // save also the preview files to the crate destination + if (crate.getPreview() != null && this.writePreview) { + crate.getPreview().saveAllToFolder(file); + } + for (var e : crate.getUntrackedFiles()) { + if (e.isDirectory()) { + FileUtils.copyDirectoryToDirectory(e, file); + } else { + FileUtils.copyFileToDirectory(e, file); + } + } + for (DataEntity dataEntity : crate.getAllDataEntities()) { + saveToFile(dataEntity, file); + } + } + + private void saveToFile(DataEntity entity, File file) throws IOException { + if (entity.getPath() != null) { + String id = entity.getId(); + String filename = IdentifierUtils.decode(id).orElse(id); + Path baseFolder = file.toPath().toAbsolutePath().normalize(); + Path destination = baseFolder.resolve(filename).normalize(); + // defence-in-depth: ensure the resolved path remains inside the crate folder + if (!destination.startsWith(baseFolder)) { + logger.warn( + "Skipping entity '{}': resolved path '{}' escapes destination folder '{}'", + id, + destination, + file + ); + return; + } + if (entity.getPath().toFile().isDirectory()) { + FileUtils.copyDirectory( + entity.getPath().toFile(), + destination.toFile() + ); + } else { + FileUtils.copyFile(entity.getPath().toFile(), destination.toFile()); + } + } + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java new file mode 100644 index 00000000..ed336e93 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java @@ -0,0 +1,34 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.Crate; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the writing strategy to provide a way of writing crates to + * a zip archive. + */ +public class WriteZipStrategy + implements GenericWriterStrategy, ElnFormatWriter +{ + + private static final Logger logger = LoggerFactory.getLogger( + WriteZipStrategy.class + ); + protected ElnFormatWriter delegate = + new WriteZipStreamStrategy(); + + @Override + public ElnFormatWriter usingElnStyle() { + this.delegate = this.delegate.withRootSubdirectory(); + return this; + } + + @Override + public void save(Crate crate, String destination) throws IOException { + this.delegate.save(crate, new FileOutputStream(destination)); + } +} diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java new file mode 100644 index 00000000..e3dcceb9 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java @@ -0,0 +1,247 @@ +package edu.kit.datamanager.ro_crate.writer; + +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.preview.CratePreview; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; +import edu.kit.datamanager.ro_crate.util.FileSystemUtil; +import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import net.lingala.zip4j.model.ZipParameters; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the writing strategy to provide a way of writing crates to + * a zip archive. + */ +public class WriteZipStreamStrategy + implements GenericWriterStrategy, ElnFormatWriter +{ + + private static final Logger logger = LoggerFactory.getLogger( + WriteZipStreamStrategy.class + ); + public static final String TMP_DIR = + "./.tmp/ro-crate-java/writer-zip-stream-strategy/"; + + /** + * Defines if the zip file will directly contain the crate, + * or if it will contain a subdirectory with the crate. + */ + protected boolean createRootSubdir = false; + + /** + * In streams, we do not have a file name yet (or do not know it), + * so we need to set a default name for the root subdirectory. + */ + protected String rootSubdirName = "content"; + + @Override + public ElnFormatWriter usingElnStyle() { + this.createRootSubdir = true; + return this; + } + + /** + * Sets the name of a root subdirectory in the zip file. + * Implicitly also enables the creation of a root subdirectory. + * If used for ELN files, note the subdirectory name should be the same as the zip + * files name. + * + * @param name the name of the subdirectory + * @return this instance of ReadZipStreamStrategy + */ + public WriteZipStreamStrategy setSubdirectoryName(String name) { + this.rootSubdirName = name; + this.createRootSubdir = true; + return this; + } + + @Override + public void save(Crate crate, OutputStream destination) throws IOException { + String innerFolderName = ""; + if (this.createRootSubdir) { + innerFolderName = FileSystemUtil.filterExtensionsFromFileName( + this.rootSubdirName, + Set.of("ELN", "ZIP") + ); + innerFolderName = FileSystemUtil.ensureTrailingSlash(innerFolderName); + } + try (ZipOutputStream zipFile = new ZipOutputStream(destination)) { + saveMetadataJson(crate, zipFile, innerFolderName); + saveDataEntities(crate, zipFile, innerFolderName); + savePreview(crate, zipFile, innerFolderName); + } + } + + private void saveDataEntities( + Crate crate, + ZipOutputStream zipStream, + String prefix + ) throws IOException { + for (DataEntity dataEntity : crate.getAllDataEntities()) { + this.saveToStream(dataEntity, zipStream, prefix); + } + } + + private void saveMetadataJson( + Crate crate, + ZipOutputStream zipStream, + String prefix + ) throws IOException { + // write the metadata.json file + ZipParameters zipParameters = new ZipParameters(); + zipParameters.setFileNameInZip(prefix + "ro-crate-metadata.json"); + // we create an JsonValue only to have the file written pretty + JsonValue node = Json.parse(crate.getJsonMetadata()); + String str = Json.writePretty(node); + // write the ro-crate-metadata + + byte[] buff = new byte[4096]; + int readLen; + zipStream.putNextEntry(zipParameters); + try ( + InputStream inputStream = new ByteArrayInputStream( + str.getBytes(StandardCharsets.UTF_8) + ) + ) { + while ((readLen = inputStream.read(buff)) != -1) { + zipStream.write(buff, 0, readLen); + } + } + zipStream.closeEntry(); + } + + private void savePreview( + Crate crate, + ZipOutputStream zipStream, + String prefix + ) throws IOException { + Optional preview = Optional.ofNullable(crate.getPreview()); + if (preview.isEmpty()) { + return; + } + final String ID = UUID.randomUUID().toString(); + File tmpPreviewFolder = Path.of(TMP_DIR).resolve(ID).toFile(); + FileUtils.forceMkdir(tmpPreviewFolder); + FileUtils.forceDeleteOnExit(tmpPreviewFolder); + + preview.get().generate(crate, tmpPreviewFolder); + String[] paths = tmpPreviewFolder.list(); + if (paths == null) { + throw new IOException( + "No preview files found in temporary folder. Preview generation failed." + ); + } + for (String path : paths) { + File file = tmpPreviewFolder.toPath().resolve(path).toFile(); + if (file.isDirectory()) { + ZipStreamUtil.addFolderToZipStream(zipStream, file, prefix + path); + } else { + ZipStreamUtil.addFileToZipStream(zipStream, file, prefix + path); + } + } + try { + FileUtils.forceDelete(tmpPreviewFolder); + } catch (IOException e) { + logger.error( + "Could not delete temporary preview folder: {}", + tmpPreviewFolder + ); + } + } + + private void saveToStream( + DataEntity entity, + ZipOutputStream zipStream, + String prefix + ) throws IOException { + if (entity == null) { + return; + } + + boolean isDirectory = entity.getPath().toFile().isDirectory(); + String id = entity.getId(); + String filename = IdentifierUtils.decode(id).orElse(id); + String safeName = sanitizeZipEntryName(filename); + if (safeName.isEmpty()) { + logger.warn( + "Skipping entity '{}': decoded name resolves outside the crate root", + id + ); + return; + } + String entryName = prefix + safeName; + if (isDirectory) { + ZipStreamUtil.addFolderToZipStream( + zipStream, + entity.getPath().toFile(), + entryName + ); + } else { + ZipStreamUtil.addFileToZipStream( + zipStream, + entity.getPath().toFile(), + entryName + ); + } + } + + /** + * Normalizes a decoded entry name into a safe relative path, consistent + * with the containment check in {@link WriteFolderStrategy#saveToFile}. + *

+ * Absolute paths (leading {@code /}) and Windows drive-qualified paths + * (e.g. {@code C:/}) are rejected. Internal {@code .} and {@code ..} + * segments are resolved; if the result escapes the virtual crate root + * the name is rejected. + * + * @param name the raw decoded entry name + * @return a canonical relative entry name, or an empty string if the name + * is absolute, drive-qualified, or escapes the crate root + */ + private static String sanitizeZipEntryName(String name) { + // zip entries always use forward slashes as separators + String normalized = name.replace('\\', '/'); + + // Reject absolute paths and Windows drive-qualified paths (e.g. C:/) + if ( + normalized.startsWith("/") || + (normalized.length() >= 2 && + normalized.charAt(1) == ':' && + Character.isLetter(normalized.charAt(0))) + ) { + return ""; + } + + // Normalize "." and ".." segments, rejecting paths that escape the root + String[] segments = normalized.split("/"); + List stack = new ArrayList<>(); + for (String segment : segments) { + if (segment.isEmpty() || segment.equals(".")) { + continue; + } + if (segment.equals("..")) { + if (stack.isEmpty()) { + return ""; + } + stack.remove(stack.size() - 1); + } else { + stack.add(segment); + } + } + return String.join("/", stack); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java new file mode 100644 index 00000000..729403c2 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java @@ -0,0 +1,42 @@ +package edu.kit.datamanager.ro_crate.writer; + +import java.io.OutputStream; + +/** + * Utility class for creating instances of different crate writers. + * This class is not meant to be instantiated. + */ +public class Writers { + + /** + * Prevents instantiation of this utility class. + */ + private Writers() {} + + /** + * Creates a new instance of a crate writer that writes to a folder. + * + * @return a new instance of {@link CrateWriter} for writing to a folder + */ + public static CrateWriter newFolderWriter() { + return new CrateWriter<>(new WriteFolderStrategy()); + } + + /** + * Creates a new instance of a crate writer that writes to a zip stream. + * + * @return a new instance of {@link CrateWriter} for writing to a zip stream + */ + public static CrateWriter newZipStreamWriter() { + return new CrateWriter<>(new WriteZipStreamStrategy()); + } + + /** + * Creates a new instance of a crate writer that writes to a zip file. + * + * @return a new instance of {@link CrateWriter} for writing to a zip file + */ + public static CrateWriter newZipPathWriter() { + return new CrateWriter<>(new WriteZipStrategy()); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java diff --git a/src/main/resources/default_context/version1.1.json b/core/src/main/resources/default_context/version1.1.json similarity index 100% rename from src/main/resources/default_context/version1.1.json rename to core/src/main/resources/default_context/version1.1.json diff --git a/src/main/resources/templates/custom_preview.ftl b/core/src/main/resources/templates/custom_preview.ftl similarity index 100% rename from src/main/resources/templates/custom_preview.ftl rename to core/src/main/resources/templates/custom_preview.ftl diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesBenchmark.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesBenchmark.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesBenchmark.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesBenchmark.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesWriteAndRead.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesWriteAndRead.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesWriteAndRead.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesWriteAndRead.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/ContextualEntitiesPerformance.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/ContextualEntitiesPerformance.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/ContextualEntitiesPerformance.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/ContextualEntitiesPerformance.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/DeletionEntitiesPerformance.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/DeletionEntitiesPerformance.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/DeletionEntitiesPerformance.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/DeletionEntitiesPerformance.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/LocalDataEntitiesPerformance.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/LocalDataEntitiesPerformance.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/LocalDataEntitiesPerformance.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/LocalDataEntitiesPerformance.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/MixtureOfEntitiesPerformance.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/MixtureOfEntitiesPerformance.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/MixtureOfEntitiesPerformance.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/MixtureOfEntitiesPerformance.java diff --git a/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/RemoteDataEntitiesPerformance.java b/core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/RemoteDataEntitiesPerformance.java similarity index 100% rename from src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/RemoteDataEntitiesPerformance.java rename to core/src/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/RemoteDataEntitiesPerformance.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java new file mode 100644 index 00000000..1fded937 --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java @@ -0,0 +1,153 @@ +package edu.kit.datamanager.ro_crate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.writer.Writers; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Golden tests locking down the exact behavior of the library as a safety net + * for the Jackson-free-core conversion. + * + *

For every fixture crate this test asserts: + *

    + *
  1. {@code getJsonMetadata()} of a freshly read crate is byte-identical to + * the committed golden file ({@code /golden/<name>.metadata.json}).
  2. + *
  3. Writing the crate to disk and reading it back yields structurally + * identical metadata (write/read idempotence, compared with the same + * comparator the existing test suite uses).
  4. + *
  5. The set of data entities survives the round trip unchanged.
  6. + *
+ * + *

If a behavior change is intentional, regenerate the golden files with + * {@code ./gradlew test -Drocrate.golden.update=true} and review the diff — + * it must be limited to the intended change. + */ +class GoldenCrateRoundTripTest { + + /** Fixtures must be plain crate folders (no network access on read). */ + static Stream fixtureCrates() { + return Stream.of( + "crates/extendedContextExample", + "crates/hasPartHierarchy", + "crates/simple_crate", + "crates/other/idrc_project", + "crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array" + ); + } + + /** + * Canonicalizes metadata for byte-level comparison: entities in the graph + * are sorted by their {@code @id}. The library does not specify the order + * of entities in the output graph (it follows Java hash iteration, which is + * not stable across JVM runs), so entity order is excluded from the lock; + * everything else — entity content, property order, formatting — is locked. + */ + static String canonicalize(String metadata) throws IOException { + JsonObject root = (JsonObject) Json.parse(metadata); + JsonValue graph = root.path("@graph"); + if (graph.isArray()) { + List entities = new ArrayList<>(); + graph.forEach(entities::add); + entities.sort( + Comparator.comparing( + node -> node.path("@id").asText(""), + String.CASE_INSENSITIVE_ORDER + ) + ); + JsonArray graphArray = (JsonArray) graph; + while (!graphArray.isEmpty()) { + graphArray.remove(0); + } + entities.forEach(graphArray::add); + } + return Json.write(root); + } + + @ParameterizedTest + @MethodSource("fixtureCrates") + void goldenMetadataIsStableThroughWriteReadRoundTrip(String fixture) + throws IOException { + Path crateDir = Path.of("src/test/resources", fixture); + String name = Path.of(fixture).getFileName().toString(); + + RoCrate read = Readers.newFolderReader().readCrate(crateDir.toString()); + String originalMetadata = read.getJsonMetadata(); + + Path goldenPath = Path.of( + "src/test/resources/golden", + name + ".metadata.json" + ); + if (System.getProperty("rocrate.golden.update") != null) { + Files.createDirectories(goldenPath.getParent()); + Files.writeString( + goldenPath, + canonicalize(originalMetadata), + StandardCharsets.UTF_8 + ); + } else { + assertTrue( + Files.exists(goldenPath), + "missing golden file " + + goldenPath + + " — run with -Drocrate.golden.update=true to generate" + ); + assertEquals( + Files.readString(goldenPath, StandardCharsets.UTF_8), + canonicalize(originalMetadata), + "metadata of freshly read crate '" + + name + + "' drifted from the golden file" + ); + } + + Path writtenDir = Files.createTempDirectory("golden-roundtrip"); + Writers.newFolderWriter().save(read, writtenDir.toString()); + RoCrate roundTripped = Readers.newFolderReader().readCrate( + writtenDir.toString() + ); + + // structural comparison: the reader normalizes formatting, so byte equality + // across a write/read round trip is not an invariant of the current library + HelpFunctions.compareTwoCrateJson(read, roundTripped); + + if (System.getProperty("rocrate.golden.update") != null) { + // golden files are regenerated; no additional assertions in this mode + } else { + } + + assertEquals( + read + .getAllDataEntities() + .stream() + .map(DataEntity::getId) + .sorted() + .toList(), + roundTripped + .getAllDataEntities() + .stream() + .map(DataEntity::getId) + .sorted() + .toList(), + "data entities changed across the write/read round trip for '" + + name + + "'" + ); + } +} diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java b/core/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java new file mode 100644 index 00000000..b640351c --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java @@ -0,0 +1,196 @@ +package edu.kit.datamanager.ro_crate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import edu.kit.datamanager.ro_crate.entities.AbstractEntity; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; +import io.json.compare.JSONCompare; +import io.json.compare.JsonComparator; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.io.FileUtils; +import org.opentest4j.AssertionFailedError; + +public class HelpFunctions { + + public static void compareEntityWithFile(AbstractEntity entity, String string) + throws IOException { + InputStream inputStream = HelpFunctions.class.getResourceAsStream(string); + JsonValue expectedJson = Json.parse(inputStream); + JsonValue node = entity.toJson(); + //compare the size of the expected and actual node. Both nodes should have the same number of properties. + assertEquals(expectedJson.size(), node.size()); + compare(expectedJson, node, true); + } + + public static void compare(JsonValue node1, JsonValue node2, Boolean equals) { + var comparator = new JsonComparator() { + public boolean compareValues(Object expected, Object actual) { + return expected.equals(actual); + } + + public boolean compareFields(String expected, String actual) { + return expected.equals(actual); + } + }; + + // json-compare works on JSON documents; hand it the string form of the + // neutral trees (produced by the active adapter) + if (equals) { + JSONCompare.assertMatches(node1.toString(), node2.toString(), comparator); + } else { + JSONCompare.assertNotMatches( + node1.toString(), + node2.toString(), + comparator + ); + } + } + + public static void compareTwoMetadataJsonNotEqual(Crate crate1, Crate crate2) + throws IOException { + JsonValue node1 = Json.parse(crate1.getJsonMetadata()); + JsonValue node2 = Json.parse(crate2.getJsonMetadata()); + compare(node1, node2, false); + } + + public static void compareTwoMetadataJsonNotEqual( + Crate crate1, + String jsonFileString + ) throws IOException { + InputStream inputStream = HelpFunctions.class.getResourceAsStream( + jsonFileString + ); + JsonValue node1 = Json.parse(crate1.getJsonMetadata()); + JsonValue node2 = JsonUtilFunctions.unwrapSingleArray( + Json.parse(inputStream) + ); + compare(node1, node2, false); + } + + public static void compareTwoCrateJson(Crate crate1, Crate crate2) + throws IOException { + JsonValue node1 = Json.parse(crate1.getJsonMetadata()); + JsonValue node2 = Json.parse(crate2.getJsonMetadata()); + compare(node1, node2, true); + } + + public static void prettyPrintJsonString(String minimalJsonMetadata) { + try { + JsonValue jsonNode = Json.parse(minimalJsonMetadata); + System.out.println(Json.writePretty(jsonNode)); + } catch (IOException e) { + throw new AssertionFailedError("Not able to process string as JSON!", e); + } + } + + public static void printAndAssertEquals( + RoCrate crate, + String pathToResource + ) { + // So you get something to see + prettyPrintJsonString(crate.getJsonMetadata()); + // Compare with the example from the specification + try { + HelpFunctions.compareCrateJsonToFileInResources(crate, pathToResource); + } catch (IOException e) { + throw new AssertionFailedError("Missing resources file!", e); + } + } + + public static void compareCrateJsonToFileInResources(File file1, File file2) + throws IOException { + JsonValue node1 = JsonUtilFunctions.unwrapSingleArray(Json.parse(file1)); + JsonValue node2 = JsonUtilFunctions.unwrapSingleArray(Json.parse(file2)); + compare(node1, node2, true); + } + + /** + * Compares the JSON metadata of a Crate object with a JSON file in the resources directory. + * + * @param crate1 The Crate object to compare. + * @param jsonFileString The path to the JSON file in the resources directory. + * @throws IOException If an error occurs while reading the JSON file. + */ + public static void compareCrateJsonToFileInResources( + Crate crate1, + String jsonFileString + ) throws IOException { + InputStream inputStream = HelpFunctions.class.getResourceAsStream( + jsonFileString + ); + JsonValue node1 = Json.parse(crate1.getJsonMetadata()); + JsonValue node2 = JsonUtilFunctions.unwrapSingleArray( + Json.parse(inputStream) + ); + compare(node1, node2, true); + } + + public static boolean compareTwoDir(File dir1, File dir2) throws IOException { + // compare the content of the two directories + Map compareWithMe = FileUtils.listFiles(dir1, null, true) + .stream() + .collect(Collectors.toMap(java.io.File::getName, Function.identity())); + + Map testMe = FileUtils.listFiles(dir2, null, true) + .stream() + .collect(Collectors.toMap(java.io.File::getName, Function.identity())); + + if (compareWithMe.size() != testMe.size()) { + return false; + } + for (String filename : testMe.keySet()) { + // we do that because the ro-crate-metadata.json can be differently formatted, + // or the order of the entities may be different + // the same holds for the html file + if ( + filename.equals("ro-crate-preview.html") || + filename.equals("ro-crate-metadata.json") + ) { + if (!compareWithMe.containsKey(filename)) { + return false; + } + } else if ( + !FileUtils.contentEqualsIgnoreEOL( + testMe.get(filename), + compareWithMe.get(filename), + null + ) + ) { + return false; + } + } + return true; + } + + /** + * Prints the file tree of the given directory for debugging and understanding + * a test more quickly. + * + * @param directoryToPrint the directory to print + * @throws IOException if an error occurs while printing the file tree + */ + @SuppressWarnings("resource") + public static void printFileTree(Path directoryToPrint) throws IOException { + // Print all files recursively in a tree structure for debugging + System.out.printf( + "Files in %s:%n", + directoryToPrint.getFileName().toString() + ); + Files.walk(directoryToPrint).forEach(path -> { + if (!path.toAbsolutePath().equals(directoryToPrint.toAbsolutePath())) { + int depth = path.relativize(directoryToPrint).getNameCount(); + String prefix = " ".repeat(depth); + System.out.printf("%s%s%s%n", prefix, "└── ", path.getFileName()); + } + }); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java similarity index 67% rename from src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index 1fb7b574..4329bf84 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -1,25 +1,20 @@ package edu.kit.datamanager.ro_crate.context; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import static org.junit.jupiter.api.Assertions.*; 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; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; import java.io.IOException; import java.util.List; import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class RoCrateMetadataContextTest { @@ -31,44 +26,45 @@ void initContext() throws IOException { // this will load the default context this.context = new RoCrateMetadataContext(); - final String crateManifestPath = "/crates/extendedContextExample/ro-crate-metadata.json"; - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode jsonNode = objectMapper.readTree(RoCrateMetadataContextTest.class.getResourceAsStream(crateManifestPath)); + final String crateManifestPath = + "/crates/extendedContextExample/ro-crate-metadata.json"; + JsonObject jsonNode = (JsonObject) Json.parse( + RoCrateMetadataContextTest.class.getResourceAsStream(crateManifestPath) + ); this.complexContext = new RoCrateMetadataContext(jsonNode.get("@context")); } @Test void testContext() { - var entity = this.context.getContextJsonEntity(); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); // we expect the default RO-crate context; - var expected = objectMapper.createObjectNode(); + var expected = Json.objectNode(); expected.put("@context", "https://w3id.org/ro/crate/1.1/context"); HelpFunctions.compare(entity, expected, true); // this entity is not correct // there is no type blabla in schema.org var contextualEntity = new ContextualEntity.ContextualEntityBuilder() - .setId("dkfaj") - .addType("blabla") - .build(); + .setId("dkfaj") + .addType("blabla") + .build(); assertFalse(this.context.checkEntity(contextualEntity)); // this entity is correct the type Accommodation exists in schema.org, // so it should be valid var contextualEntityCorrect = new ContextualEntity.ContextualEntityBuilder() - .setId("dkfaj") - .addType("Accommodation") - .addProperty("name", "ffjkjk") - .build(); + .setId("dkfaj") + .addType("Accommodation") + .addProperty("name", "ffjkjk") + .build(); assertTrue(this.context.checkEntity(contextualEntityCorrect)); // this entity has correct types but wrong field - var contextualEntityWrongField = new ContextualEntity.ContextualEntityBuilder() + var contextualEntityWrongField = + new ContextualEntity.ContextualEntityBuilder() .setId("dkfaj") .addType("Accommodation") .addProperty("notExisting", "kfdjfk") @@ -80,9 +76,11 @@ void testContext() { @Test void creationUrlTest() { RoCrateMetadataContext newContext = new RoCrateMetadataContext( - List.of("www.example.com", "www.example.com/context")); + List.of("www.example.com", "www.example.com/context") + ); - var contextualEntityWrongField = new ContextualEntity.ContextualEntityBuilder() + var contextualEntityWrongField = + new ContextualEntity.ContextualEntityBuilder() .setId("dkfaj") .addType("Accommodation") .build(); @@ -96,13 +94,11 @@ void creationUrlTest() { @Test void creationFromPairsJsonTest() { - var objectMapper = MyObjectMapper.getMapper(); - - ObjectNode rawContext = objectMapper.createObjectNode(); + JsonObject rawContext = Json.objectNode(); rawContext.put("house", "www.example.com/house"); rawContext.put("road", "www.example.com/road"); - ObjectNode rawCrate = objectMapper.createObjectNode(); + JsonObject rawCrate = Json.objectNode(); rawCrate.set("@context", rawContext); RoCrateMetadataContext newContext = new RoCrateMetadataContext(rawContext); assertNotNull(newContext); @@ -110,10 +106,10 @@ void creationFromPairsJsonTest() { HelpFunctions.compare(newContext.getContextJsonEntity(), rawCrate, true); var entityWithTerms = new ContextualEntity.ContextualEntityBuilder() - .setId("dkfaj") - .addType("house") - .addType("road") - .build(); + .setId("dkfaj") + .addType("house") + .addType("road") + .build(); assertTrue(newContext.checkEntity(entityWithTerms)); } @@ -126,7 +122,11 @@ void deletePairTest() { context.deleteValuePairFromContext("key"); - HelpFunctions.compare(context.getContextJsonEntity(), emptyContext.getContextJsonEntity(), true); + HelpFunctions.compare( + context.getContextJsonEntity(), + emptyContext.getContextJsonEntity(), + true + ); } @Test @@ -136,7 +136,11 @@ void deleteNonExistingPairTest() { context.deleteValuePairFromContext("key"); - HelpFunctions.compare(context.getContextJsonEntity(), emptyContext.getContextJsonEntity(), true); + HelpFunctions.compare( + context.getContextJsonEntity(), + emptyContext.getContextJsonEntity(), + true + ); } @Test @@ -148,8 +152,11 @@ void deleteUrlTest() { context.deleteUrlFromContext("www.example.com"); - HelpFunctions.compare(context.getContextJsonEntity(), emptyContext.getContextJsonEntity(), true); - + HelpFunctions.compare( + context.getContextJsonEntity(), + emptyContext.getContextJsonEntity(), + true + ); } @Test @@ -163,13 +170,21 @@ void doubledContextUrlsTest() { RoCrateMetadataContext contextDoubled = new RoCrateMetadataContext(); contextDoubled.addToContextFromUrl(url); contextDoubled.addToContextFromUrl(url); - - HelpFunctions.compare(context.getContextJsonEntity(), contextDoubled.getContextJsonEntity(), true); + + HelpFunctions.compare( + context.getContextJsonEntity(), + contextDoubled.getContextJsonEntity(), + true + ); RoCrateMetadataContext emptyContext = new RoCrateMetadataContext(); contextDoubled.deleteUrlFromContext(url); - HelpFunctions.compare(emptyContext.getContextJsonEntity(), contextDoubled.getContextJsonEntity(), true); + HelpFunctions.compare( + emptyContext.getContextJsonEntity(), + contextDoubled.getContextJsonEntity(), + true + ); } @Test @@ -179,18 +194,19 @@ void deleteNonExistentUrlTest() { context.deleteUrlFromContext("www.example.com"); - HelpFunctions.compare(context.getContextJsonEntity(), emptyContext.getContextJsonEntity(), true); + HelpFunctions.compare( + context.getContextJsonEntity(), + emptyContext.getContextJsonEntity(), + true + ); } @Test void creationFromUrlAndPairsTest() { + JsonObject res = Json.objectNode(); + JsonArray arr = Json.arrayNode(); - var objectMapper = MyObjectMapper.getMapper(); - - ObjectNode res = objectMapper.createObjectNode(); - ArrayNode arr = objectMapper.createArrayNode(); - - ObjectNode node = objectMapper.createObjectNode(); + JsonObject node = Json.objectNode(); node.put("house", "www.example.con/house"); node.put("road", "www.example.con/road"); arr.add(node); @@ -201,9 +217,9 @@ void creationFromUrlAndPairsTest() { HelpFunctions.compare(newContext.getContextJsonEntity(), res, true); var data = new DataEntity.DataEntityBuilder() - .addType("house") - .setId("https://www.example.com/entity") - .build(); + .addType("house") + .setId("https://www.example.com/entity") + .build(); // house is in the context assertTrue(newContext.checkEntity(data)); } @@ -211,33 +227,33 @@ void creationFromUrlAndPairsTest() { @Test void testIdType() { AbstractEntity validEntity = new DataEntity.DataEntityBuilder() - .setId("Airline") // this is defined in the context! - .addType("@id") // this is a JSON-LD feature to refer to the ID ("Airline") as a type - .addType("@json") // this is a JSON-LD built-in type - .build(); + .setId("Airline") // this is defined in the context! + .addType("@id") // this is a JSON-LD feature to refer to the ID ("Airline") as a type + .addType("@json") // this is a JSON-LD built-in type + .build(); assertTrue(this.context.checkEntity(validEntity)); AbstractEntity invalidEntity = new DataEntity.DataEntityBuilder() - .setId("Something which is definitely not in the context") - .addType("@id") - .build(); + .setId("Something which is definitely not in the context") + .addType("@id") + .build(); assertFalse(this.context.checkEntity(invalidEntity)); } @Test void testJsonType() { AbstractEntity validEntity = new DataEntity.DataEntityBuilder() - .addType("@json") // this is a JSON-LD built-in type - .build(); + .addType("@json") // this is a JSON-LD built-in type + .build(); assertTrue(this.context.checkEntity(validEntity)); } @Test void testAbsoluteUrlType() { AbstractEntity validEntity = new DataEntity.DataEntityBuilder() - .addType("http://example.org/Person") // this is not in the context! - .addProperty("http://example.org/Thing", "Some thing") - .build(); + .addType("http://example.org/Person") // this is not in the context! + .addProperty("http://example.org/Thing", "Some thing") + .build(); assertTrue(this.context.checkEntity(validEntity)); } @@ -267,10 +283,15 @@ void testReadKeys() { var expected = Set.of("custom", "owl", "datacite", "xsd", "rdfs"); var given = this.complexContext.getKeys(); for (String key : expected) { - assertTrue(given.contains(key), "Key " + key + " not found in the context"); + assertTrue( + given.contains(key), + "Key " + key + " not found in the context" + ); } // prove immutability - assertThrows(UnsupportedOperationException.class, () -> given.add("newKey")); + assertThrows(UnsupportedOperationException.class, () -> + given.add("newKey") + ); } @Test @@ -280,11 +301,18 @@ void testReadPairs() { var keys = given.keySet(); var values = given.values(); for (String key : expected) { - assertTrue(keys.contains(key), "Key " + key + " not found in the context"); - values.forEach(s -> assertFalse(s.isEmpty(), "Value for key " + key + " is empty")); + assertTrue( + keys.contains(key), + "Key " + key + " not found in the context" + ); + values.forEach(s -> + assertFalse(s.isEmpty(), "Value for key " + key + " is empty") + ); } // prove immutability - assertThrows(UnsupportedOperationException.class, () -> given.put("newKey", "newValue")); + assertThrows(UnsupportedOperationException.class, () -> + given.put("newKey", "newValue") + ); } @Test @@ -292,13 +320,13 @@ 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(); + 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(); + .addType("rdfs:Property") + .build(); crate.addDataEntity(entity); // Then we expect this to work assertTrue(context.checkEntity(entity)); diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java new file mode 100644 index 00000000..55343b9c --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java @@ -0,0 +1,88 @@ +package edu.kit.datamanager.ro_crate.crate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.special.CrateVersion; +import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import org.junit.jupiter.api.Test; + +class BuilderSpec12Test { + + private URI profile1; + private URI profile2; + + @Test + void testAppendConformsTo() throws URISyntaxException { + Crate crate = new RoCrate.BuilderWithDraftFeatures() + .alsoConformsTo(new URI("https://w3id.org/ro/wfrun/process/0.1")) + .alsoConformsTo(new URI("https://example.com/myprofile/1.0")) + .build(); + JsonValue conformsTo = crate.getJsonDescriptor().getProperty("conformsTo"); + assertTrue(conformsTo.isArray()); + // one version and two profiles + assertEquals(1 + 2, conformsTo.size()); + } + + @Test + void testModificationOfDraftCrate() throws URISyntaxException, IOException { + String path = this.getClass() + .getResource("/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array") + .getPath(); + RoCrate crate = Readers.newFolderReader().readCrate(path); + Collection existingProfiles = crate.getProfiles(); + profile1 = new URI("https://example.com/myprofile/1.0"); + profile2 = new URI("https://example.com/myprofile/2.0"); + // the loaded crate has at least one profile + assertFalse(existingProfiles.isEmpty()); + // and the ones we will add later are not part of it + assertFalse(existingProfiles.contains(profile1.toString())); + assertFalse(existingProfiles.contains(profile2.toString())); + + // add profiles + RoCrate modifiedCrate = new RoCrate.BuilderWithDraftFeatures(crate) + .alsoConformsTo(profile1) + .alsoConformsTo(profile2) + .addContextualEntity( + new ContextualEntity.ContextualEntityBuilder() + .addType("CreativeWork") + .build() + ) + .build(); + + // sanity checks + Validator defaultValidation = new Validator(new JsonSchemaValidation()); + assertTrue(defaultValidation.validate(modifiedCrate)); + assertEquals(CrateVersion.LATEST_UNSTABLE, crate.getVersion().get()); + assertEquals( + CrateVersion.LATEST_UNSTABLE, + modifiedCrate.getVersion().get() + ); + + // number of profiles increased by 2 + Collection newProfileState = modifiedCrate.getProfiles(); + assertEquals(existingProfiles.size() + 2, newProfileState.size()); + // new profiles are present + assertTrue(newProfileState.contains(profile1.toString())); + assertTrue(newProfileState.contains(profile2.toString())); + // old profiles are present + assertEquals( + 0, + existingProfiles + .stream() + .filter(txt -> !newProfileState.contains(txt)) + .count() + ); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java similarity index 95% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java index d4fed2a9..76ec126a 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.JsonProcessingException; import edu.kit.datamanager.ro_crate.HelpFunctions; import edu.kit.datamanager.ro_crate.RoCrate; @@ -14,7 +13,7 @@ class BuilderTest { @Test - void testReadBuilder() throws JsonProcessingException { + void testReadBuilder() throws IOException { ContextualEntity license = new ContextualEntity.ContextualEntityBuilder() .addType("CreativeWork") .setId("https://creativecommons.org/licenses/by-nc-sa/3.0/au/") @@ -56,7 +55,7 @@ void testAddCrateWithOnlyRootDataEntity() throws IOException { } @Test - void testEmptyCrates() throws JsonProcessingException { + void testEmptyCrates() throws IOException { RoCrate built = new RoCrate.RoCrateBuilder().build(); RoCrate constructed = new RoCrate(); diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/HasPartTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/HasPartTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/HasPartTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/HasPartTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/OtherFilesTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/OtherFilesTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/OtherFilesTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/OtherFilesTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/SerializationTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/SerializationTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/SerializationTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/SerializationTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java similarity index 93% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java index 31c0dc8e..9b051d99 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java @@ -4,7 +4,6 @@ import edu.kit.datamanager.ro_crate.HelpFunctions; import edu.kit.datamanager.ro_crate.RoCrate; -import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; import java.util.Objects; @@ -25,7 +24,7 @@ void setup() throws IOException { } @Test - void testAddRemoveValuePair() throws JsonProcessingException { + void testAddRemoveValuePair() throws IOException { RoCrate crate = new RoCrate.RoCrateBuilder().addValuePairToContext("key", "value").build(); RoCrate defaultCrate = new RoCrate.RoCrateBuilder().build(); @@ -36,7 +35,7 @@ void testAddRemoveValuePair() throws JsonProcessingException { } @Test - void testAddRemoveUrl() throws JsonProcessingException { + void testAddRemoveUrl() throws IOException { RoCrate crate = new RoCrate.RoCrateBuilder().addUrlToContext("https://example.com").build(); RoCrate defaultCrate = new RoCrate.RoCrateBuilder().build(); diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java new file mode 100644 index 00000000..037d306f --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java @@ -0,0 +1,213 @@ +package edu.kit.datamanager.ro_crate.crate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.PersonEntity; +import edu.kit.datamanager.ro_crate.entities.contextual.PlaceEntity; +import edu.kit.datamanager.ro_crate.entities.data.FileEntity; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +public class TestRemoveAddEntity { + + @Test + void testAddRemoveEntity() throws IOException { + RoCrate roCrate = new RoCrate.RoCrateBuilder( + "minimal", + "minimal RO_crate", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addDataEntity( + new FileEntity.FileEntityBuilder() + .setId("survey-responses-2019.csv") + .addProperty("name", "Survey responses") + .addProperty("contentSize", "26452") + .addProperty("encodingFormat", "text/csv") + .build() + ) + .build(); + HelpFunctions.compareCrateJsonToFileInResources( + roCrate, + "/json/crate/onlyOneFile.json" + ); + + assertEquals(0, roCrate.getAllContextualEntities().size()); + assertEquals(1, roCrate.getAllDataEntities().size()); + + // remove entity and check if equals to the basic crate + roCrate.deleteEntityById("survey-responses-2019.csv"); + assertEquals(0, roCrate.getAllDataEntities().size()); + HelpFunctions.compareCrateJsonToFileInResources( + roCrate, + "/json/crate/simple.json" + ); + } + + @Test + void withTwoFiles() throws IOException { + PlaceEntity place = new PlaceEntity.PlaceEntityBuilder() + .setId("http://sws.geonames.org/8152662/") + .addProperty("name", "Catalina Park") + .build(); + + PersonEntity person = new PersonEntity.PersonEntityBuilder() + .setId("#alice") + .addProperty("name", "Alice") + .addProperty("description", "One of hopefully many Contextual Entities") + .build(); + + FileEntity file = new FileEntity.FileEntityBuilder() + .setId("data1.txt") + .addProperty("description", "One of hopefully many Data Entities") + .setContentLocation(place.getId()) + .addAuthor(person.getId()) + .build(); + + RoCrate roCrate = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addContextualEntity(place) + .addContextualEntity(person) + .addDataEntity(file) + .addDataEntity( + new FileEntity.FileEntityBuilder().setId("data2.txt").build() + ) + .build(); + + HelpFunctions.compareCrateJsonToFileInResources( + roCrate, + "/json/crate/twoFiles.json" + ); + + assertEquals(2, roCrate.getAllDataEntities().size()); + assertEquals(2, roCrate.getAllContextualEntities().size()); + + roCrate.deleteEntityById("data1.txt"); + assertEquals(1, roCrate.getAllDataEntities().size()); + + roCrate.deleteEntityById("data2.txt"); + assertEquals(0, roCrate.getAllDataEntities().size()); + + roCrate.deleteEntityById("#alice"); + assertEquals(1, roCrate.getAllContextualEntities().size()); + + roCrate.deleteEntityById("http://sws.geonames.org/8152662/"); + assertEquals(0, roCrate.getAllContextualEntities().size()); + + RoCrate roCrate2 = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ).build(); + HelpFunctions.compareTwoCrateJson(roCrate, roCrate2); + } + + @Test + void removeOtherOccur() throws IOException { + PlaceEntity place = new PlaceEntity.PlaceEntityBuilder() + .setId("http://sws.geonames.org/8152662/") + .addProperty("name", "Catalina Park") + .build(); + + PersonEntity person = new PersonEntity.PersonEntityBuilder() + .setId("#alice") + .addProperty("name", "Alice") + .addProperty("description", "One of hopefully many Contextual Entities") + .build(); + + FileEntity file = new FileEntity.FileEntityBuilder() + .setId("data1.txt") + .addProperty("description", "One of hopefully many Data Entities") + .setContentLocation(place.getId()) + .addAuthor(person.getId()) + .addAuthor(person.getId()) + .build(); + + RoCrate roCrate = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addContextualEntity(place) + .addContextualEntity(person) + .addDataEntity(file) + .build(); + + assertEquals(1, roCrate.getAllDataEntities().size()); + assertEquals(2, roCrate.getAllContextualEntities().size()); + + roCrate.deleteEntityById(person.getId()); + assertEquals(1, roCrate.getAllContextualEntities().size()); + + roCrate.deleteEntityById(place.getId()); + assertEquals(0, roCrate.getAllContextualEntities().size()); + assertEquals(1, roCrate.getAllDataEntities().size()); + + FileEntity file2 = new FileEntity.FileEntityBuilder() + .setId("data1.txt") + .addProperty("description", "One of hopefully many Data Entities") + .build(); + RoCrate second2 = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addDataEntity(file2) + .build(); + + HelpFunctions.compareTwoCrateJson(roCrate, second2); + } + + @Test + void testAddEntitiesWithSameId() throws IOException { + PersonEntity person = new PersonEntity.PersonEntityBuilder() + .setId("person") + .addProperty("name", "Alice") + .addProperty("description", "One of hopefully many Contextual Entities") + .build(); + + RoCrate roCrate = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addContextualEntity(person) + .build(); + + RoCrate roCrateNew = new RoCrate.RoCrateBuilder( + "Example RO-Crate", + "The RO-Crate Root Data Entity", + "2023", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addContextualEntity(person) + .build(); + + assertEquals(1, roCrate.getAllContextualEntities().size()); + assertEquals(1, roCrateNew.getAllContextualEntities().size()); + + PersonEntity personNew = new PersonEntity.PersonEntityBuilder() + .setId("person") + .addProperty("name", "Peter Sefton") + .addProperty("email", "peter.sefton@uts.edu.au") + .build(); + + roCrateNew.addContextualEntity(personNew); + + assertEquals(1, roCrate.getAllContextualEntities().size()); + assertEquals(1, roCrateNew.getAllContextualEntities().size()); + + assertEquals(roCrate.getContextualEntityById("person"), person); + assertEquals(roCrateNew.getContextualEntityById("person"), personNew); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/preview/PreviewCrateTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/preview/PreviewCrateTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/preview/PreviewCrateTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/preview/PreviewCrateTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/RealTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/RealTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/RealTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/RealTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/WorkflowHubTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/WorkflowHubTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/WorkflowHubTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/WorkflowHubTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java similarity index 61% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java index 170ff09c..f681f1e1 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java @@ -1,46 +1,49 @@ package edu.kit.datamanager.ro_crate.crate.validation; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.entities.data.WorkflowEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; import edu.kit.datamanager.ro_crate.validation.Validator; - -import org.junit.jupiter.api.Test; - import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.Objects; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; public class ValidationTest { @Test void jsonSchemaValidationTest() throws IOException, URISyntaxException { - Crate crate = new RoCrate.RoCrateBuilder("workflowCrate", "this is a test", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addDataEntity( - new WorkflowEntity.WorkflowEntityBuilder() - .setId("https://www.example.com/entity") - .build() - ) - .build(); - - InputStream inputStream = - ValidationTest.class.getResourceAsStream("/crates/validation/workflowschema.json"); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode expectedJson = objectMapper.readTree(inputStream); + Crate crate = new RoCrate.RoCrateBuilder( + "workflowCrate", + "this is a test", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .addDataEntity( + new WorkflowEntity.WorkflowEntityBuilder() + .setId("https://www.example.com/entity") + .build() + ) + .build(); + + InputStream inputStream = ValidationTest.class.getResourceAsStream( + "/crates/validation/workflowschema.json" + ); + JsonValue expectedJson = Json.parse(inputStream); Validator validator = new Validator(new JsonSchemaValidation(expectedJson)); assertTrue(validator.validate(crate)); - URL schemaUrl = Objects.requireNonNull(ValidationTest.class.getResource("/crates/validation/workflowschema.json")); + URL schemaUrl = Objects.requireNonNull( + ValidationTest.class.getResource("/crates/validation/workflowschema.json") + ); String schemaPath = schemaUrl.getPath(); // test with string file location validator = new Validator(new JsonSchemaValidation(schemaPath)); diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java new file mode 100644 index 00000000..efc32a7b --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java @@ -0,0 +1,148 @@ +package edu.kit.datamanager.ro_crate.entities; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonString; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class AbstractEntityTest { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { " ", "\t", "\n" }) + void mergeIdIntoValue_withInvalidId_returnsEmpty(String invalidId) { + Optional result = AbstractEntity.mergeIdIntoValue( + invalidId, + null + ); + assertTrue(result.isEmpty(), "Should return empty Optional for invalid ID"); + + Optional result2 = AbstractEntity.mergeIdIntoValue( + invalidId, + Json.objectNode() + ); + assertTrue( + result2.isEmpty(), + "Should return empty Optional for invalid ID" + ); + } + + @Test + void mergeIdIntoValue_withNullCurrentValue_returnsIdObject() { + String id = "test-id"; + Optional result = AbstractEntity.mergeIdIntoValue(id, null); + + // Should return a value for valid ID + JsonValue node = result.orElseThrow(); + assertTrue(node.isObject(), "Should return an object"); + assertEquals(id, node.get("@id").asText(), "Should contain the ID"); + } + + @Test + void mergeIdIntoValue_withExistingIdAsString_returnsEmpty() { + String id = "test-id"; + JsonValue currentValue = new JsonString(id); + + Optional result = AbstractEntity.mergeIdIntoValue( + id, + currentValue + ); + assertTrue( + result.isEmpty(), + "Should return empty when ID already exists as string" + ); + } + + @Test + void mergeIdIntoValue_withExistingIdObject_returnsEmpty() { + String id = "test-id"; + JsonObject currentValue = Json.objectNode(); + currentValue.put("@id", id); + + Optional result = AbstractEntity.mergeIdIntoValue( + id, + currentValue + ); + assertTrue( + result.isEmpty(), + "Should return empty when ID already exists as object" + ); + } + + @Test + void mergeIdIntoValue_withNonArrayValue_createsArray() { + String id = "new-id"; + String existingId = "existing-id"; + JsonObject existingValue = Json.objectNode(); + existingValue.put("@id", existingId); + + Optional result = AbstractEntity.mergeIdIntoValue( + id, + existingValue + ); + + assertTrue(result.isPresent(), "Should return a value"); + JsonValue node = result.get(); + assertTrue(node.isArray(), "Should be converted to array"); + assertEquals(2, node.size(), "Should contain both values"); + assertEquals( + existingId, + node.get(0).get("@id").asText(), + "Should contain existing ID" + ); + assertEquals(id, node.get(1).get("@id").asText(), "Should contain new ID"); + } + + @Test + void mergeIdIntoValue_withExistingArray_addsToArray() { + String id = "new-id"; + String existingId = "existing-id"; + + JsonArray currentValue = Json.arrayNode(); + JsonObject existingIdObj = Json.objectNode(); + existingIdObj.put("@id", existingId); + currentValue.add(existingIdObj); + + Optional result = AbstractEntity.mergeIdIntoValue( + id, + currentValue + ); + + assertTrue(result.isPresent(), "Should return a value"); + JsonValue node = result.get(); + assertTrue(node.isArray(), "Should remain an array"); + assertEquals(2, node.size(), "Should contain both values"); + assertEquals( + existingId, + node.get(0).get("@id").asText(), + "Should contain existing ID" + ); + assertEquals(id, node.get(1).get("@id").asText(), "Should contain new ID"); + } + + @Test + void mergeIdIntoValue_withExistingArrayContainingId_returnsEmpty() { + String id = "test-id"; + JsonArray currentValue = Json.arrayNode(); + JsonObject existingIdObj = Json.objectNode(); + existingIdObj.put("@id", id); + currentValue.add(existingIdObj); + + Optional result = AbstractEntity.mergeIdIntoValue( + id, + currentValue + ); + assertTrue( + result.isEmpty(), + "Should return empty when ID already exists in array" + ); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntityTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java new file mode 100644 index 00000000..c990de6f --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java @@ -0,0 +1,148 @@ +package edu.kit.datamanager.ro_crate.entities.contextual; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** + * @author Nikola Tzotchev on 5.2.2022 г. + * @version 1 + */ +public class ContextualEntityTest { + + /** + * This is exactly the same test as the PlaceEntityTest it shows again (similarly to the + * DataEntityTest) that we can also just use this class for the creation of our Contextual + * Entities + */ + @Test + void testSerialization() throws IOException { + // this does not make any difference for our testcase it just shows how the GeoCoordinates entity will look + ContextualEntity geo = new ContextualEntity.ContextualEntityBuilder() + .setId("#b4168a98-8534-4c6d-a568-64a55157b656") + .addType("GeoCoordinates") + .addProperty("latitude", "-33.7152") + .addProperty("longitude", "150.30119") + .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") + .build(); + + ContextualEntity place = new ContextualEntity.ContextualEntityBuilder() + .addType("Place") + .setId("https://sws.geonames.org/8152662/") + .addProperty( + "description", + "Catalina Park is a disused motor racing venue, located at Katoomba ..." + ) + .addProperty("identifier", "https://sws.geonames.org/8152662/") + .addProperty("uri", "https://www.geonames.org/8152662/catalina-park.html") + .addProperty("name", "Catalina Park") + // here we can also do .setGeo(geo) + .addIdProperty("geo", geo) + .build(); + + assertTrue(place.getLinkedTo().contains(geo.getId())); + HelpFunctions.compareEntityWithFile( + place, + "/json/entities/contextual/place.json" + ); + } + + @Test + void testAddAllValidCase() throws IOException { + ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() + .setId("#b4168a98-8534-4c6d-a568-64a55157b656") + .addType("GeoCoordinates") + .addProperty("latitude", "-33.7152") + .addProperty("longitude", "150.30119") + .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") + .build(); + + String allProperties = """ + { + "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", + "@type": "GeoCoordinates", + "latitude": "-33.7152", + "longitude": "150.30119", + "name": "Latitude: -33.7152 Longitude: 150.30119" + } + """; + + JsonObject properties = (JsonObject) Json.parse(allProperties); + ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() + .setAllIfValid(properties) + .build(); + assertEquals(second.getProperties(), first.getProperties()); + } + + @Test + void testAddAllInvalidCase() throws IOException { + ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() + .setId("#b4168a98-8534-4c6d-a568-64a55157b656") + .addType("GeoCoordinates") + .addProperty("latitude", "-33.7152") + .addProperty("longitude", "150.30119") + .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") + .build(); + + String allProperties = """ + { + "wrong property": {"any": "value"}, + "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", + "@type": "GeoCoordinates", + "latitude": "-33.7152", + "longitude": "150.30119", + "name": "Latitude: -33.7152 Longitude: 150.30119" + } + """; + + JsonObject properties = (JsonObject) Json.parse(allProperties); + ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() + .setId("second") + .setAllIfValid(properties) + .build(); + assertNotEquals(second.getProperties(), first.getProperties()); + JsonObject empty = new ContextualEntity.ContextualEntityBuilder() + .setId("second") + .build() + .getProperties(); + assertEquals(empty, second.getProperties()); + } + + @Test + void testAddAllUnsafeDoesInvalidCase() throws IOException { + ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() + .setId("#b4168a98-8534-4c6d-a568-64a55157b656") + .addType("GeoCoordinates") + .addProperty("latitude", "-33.7152") + .addProperty("longitude", "150.30119") + .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") + .build(); + + String allProperties = """ + { + "wrong property": {"any": "value"}, + "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", + "@type": "GeoCoordinates", + "latitude": "-33.7152", + "longitude": "150.30119", + "name": "Latitude: -33.7152 Longitude: 150.30119" + } + """; + + JsonObject properties = (JsonObject) Json.parse(allProperties); + ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() + .setId("second") + .setAllUnsafe(properties) + .build(); + assertNotEquals(second.getProperties(), first.getProperties()); + JsonObject empty = new ContextualEntity.ContextualEntityBuilder() + .setId("second") + .build() + .getProperties(); + assertNotEquals(empty, second.getProperties()); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntityTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntityTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntityTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java new file mode 100644 index 00000000..bddc3ba7 --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java @@ -0,0 +1,257 @@ +package edu.kit.datamanager.ro_crate.entities.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity.DataEntityBuilder; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonNull; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * @author Nikola Tzotchev on 4.2.2022 г. + * @version 1 + */ +class DataEntityTest { + + @Test + void testSerialization() throws IOException { + JsonObject bigJson = Json.objectNode(); + bigJson.put("onefield", "dalkfa"); + JsonObject address = Json.objectNode(); + address.put("street", "x"); + address.put("city", "other"); + address.put("state", "this"); + address.put("zipCode", 66439); + bigJson.set("nestedObject", address); + + DataEntity file = new DataEntityBuilder() + .addType("File") + .setId("https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf") + .addProperty("name", "RO-Crate specification") + .addProperty("encodingFormat", "application/pdf") + .addProperty("url", "https://zenodo.org/record/3541888") + // this should not be included since it does not follow the json flatten form of the RO-Crate + .addProperty("test", bigJson) + .build(); + assertNull(file.getProperty("test")); + HelpFunctions.compareEntityWithFile( + file, + "/json/entities/data/fileEntity.json" + ); + } + + @Test + void testSerializationOutside() throws IOException { + JsonObject json = Json.objectNode(); + json.put("name", "RO-Crate specification"); + json.put("encodingFormat", "application/pdf"); + json.put("url", "https://zenodo.org/record/3541888"); + json.put( + "@id", + "https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf" + ); + json.put("@type", "File"); + + DataEntity data = new DataEntity.DataEntityBuilder().build(); + data.setProperties(json); + HelpFunctions.compareEntityWithFile( + data, + "/json/entities/data/fileEntity.json" + ); + + JsonObject address = Json.objectNode(); + address.put("street", "x"); + address.put("city", "other"); + address.put("state", "this"); + address.put("zipCode", 66439); + json.set("nestedObject", address); + // this should not be included into the entity since it does not follow the basic RO-Crate structure + data.setProperties(json); + assertNotEquals("", data.getId()); + HelpFunctions.compareEntityWithFile( + data, + "/json/entities/data/fileEntity.json" + ); + } + + @Test + void testUriCheck() { + //this entity id is a valid URL so there should not be any console information + DataEntity dataEntity = new DataEntity.DataEntityBuilder() + .addType("File") + .setLocation(URI.create("https://www.example.com/19")) + .build(); + + assertNotNull(dataEntity); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> + new DataEntity.DataEntityBuilder() + .addType("File") + .setLocationWithExceptions(URI.create("zzz://wrong/url")) + .build() + ); + + assertEquals( + "This Data Entity remote ID does not resolve to a valid URL.", + exception.getMessage() + ); + + assertNotNull(dataEntity); + } + + @Test + void testPathCheck() throws IOException { + FileEntity dataEntity = new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(Paths.get("example.json")) + .setId("example.json") + .addProperty("name", "RO-Crate specification") + .setEncodingFormat("application/json") + .build(); + assertEquals("example.json", dataEntity.getId()); + HelpFunctions.compareEntityWithFile( + dataEntity, + "/json/entities/data/localFile.json" + ); + + String fileName = "cp7glop.ai"; + Path filePath = Paths.get(fileName); + + dataEntity = new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(filePath) + .setId(fileName) + .addProperty("name", "Diagram showing trend to increase") + .setEncodingFormat("application/pdf") + .build(); + assertEquals(fileName, dataEntity.getId()); + assertEquals(dataEntity.getPath(), filePath); + assertEquals( + "application/pdf", + dataEntity.getProperty("encodingFormat").asText() + ); + } + + @Test + void testEncodedUrl() throws URISyntaxException { + //this entity id is a valid URL so there should not be any console information + DataEntity dataEntity = new DataEntity.DataEntityBuilder() + .addType("File") + .setLocation( + URI.create("https://github.com/kit-data-manager/ro-crate-java/issues/5") + ) + .build(); + + assertEquals( + "https://github.com/kit-data-manager/ro-crate-java/issues/5", + dataEntity.getId() + ); + + URL url = HelpFunctions.class.getResource("/json/crate/simple2.json"); + assert url != null; + dataEntity = new DataEntity.DataEntityBuilder() + .addType("File") + .setLocationWithExceptions(Paths.get(url.toURI())) + .setId("simple2.json") + .build(); + + assertNotNull(dataEntity); + + dataEntity = new DataEntity.DataEntityBuilder() + .addType("File") + .setLocationWithExceptions( + Paths.get("/json/crate/Results%20and%20Diagrams/almost-50%25.json") + ) + .setId("almost-50%25.json") + .build(); + + assertTrue(dataEntity.getTypes().contains("File")); + assertEquals("almost-50%25.json", dataEntity.getId()); + + // even if the Path is not correctly encoded, the data entity will be added with an encoded Path. + dataEntity = new DataEntity.DataEntityBuilder() + .addType("File") + .setLocationWithExceptions( + Paths.get("/json/crate/Results and Diagrams/almost-50%.json") + ) + .setId("almost-50%.json") + .build(); + assertEquals("almost-50%25.json", dataEntity.getId()); + } + + @Test + void testRemoveProperty() { + FileEntity dataEntity = new FileEntity.FileEntityBuilder() + .addProperty("name", "RO-Crate specification") + .setEncodingFormat("application/json") + .addProperty("url", "https://zenodo.org/record/3541888") + .addProperty( + "@id", + "https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf" + ) + .build(); + + dataEntity.removeProperty("url"); + assertNull(dataEntity.getProperty("url")); + assertEquals(4, dataEntity.getProperties().size()); + + List keyList = Arrays.asList("encodingFormat", "name"); + dataEntity.removeProperties(keyList); + assertEquals(2, dataEntity.getProperties().size()); + } + + @Test + void testMixedArrayProperties() { + JsonArray propertyValue = Json.arrayNode(); + propertyValue + .add(1) + .add("2") + .add(3.14) + .add(true) + .add((JsonValue) null) + .add(JsonNull.INSTANCE); + String propertyName = "mixedArray"; + + DataEntity entity = new DataEntityBuilder() + .addProperty(propertyName, propertyValue) + .build(); + assertEquals(propertyValue, entity.getProperty(propertyName)); + } + + @ParameterizedTest + @MethodSource("provideInvalidPropertyValues") + void testNestedArrayProperties(JsonArray propertyValue) { + String propertyName = "invalidArray"; + DataEntity entity = new DataEntityBuilder() + .addProperty(propertyName, propertyValue) + .build(); + assertNull(entity.getProperty(propertyName)); + } + + private static Stream provideInvalidPropertyValues() { + JsonArray withSubArray = Json.arrayNode().add(1).add("2"); + withSubArray.add(JsonArray.create()); + JsonArray withSubObject = Json.arrayNode().add(1).add("2"); + withSubObject.add(JsonObject.create()); + + return Stream.of(withSubArray, withSubObject); + } +} diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java new file mode 100644 index 00000000..b0fb17eb --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java @@ -0,0 +1,115 @@ +package edu.kit.datamanager.ro_crate.entities.data; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +/** + * @author Nikola Tzotchev on 5.2.2022 г. + * @version 1 + */ +public class DataSetEntityTest { + + @Test + void testImpossibleRootId() { + DataSetEntity e = new DataSetEntity.DataSetBuilder() + .setId("./") + .addProperty("not_root", true) + .build(); + assertNotNull(e.getId()); + assertFalse(e.getId().isBlank()); + assertNotEquals(RootDataEntity.ID, e.getId()); + } + + @Test + void testSimpleDirDeserialization() throws IOException { + String id = "lots_of_little_files/"; + DataSetEntity dir = new DataSetEntity.DataSetBuilder() + // this does not change anything, but it shows that the entity contains a physical file + .setLocationWithExceptions(Paths.get("invalid_file")) + .setId(id) + .addProperty("name", "Too many files") + .addProperty( + "description", + "This directory contains many small files, that we're not going to describe in detail." + ) + .build(); + + assertEquals(id, dir.getId()); + HelpFunctions.compareEntityWithFile( + dir, + "/json/entities/data/directory.json" + ); + } + + /** + * This is a test with a directory that is located on the web. It is + * recommended that such a dir list all of its files in the hasPart property + * https://www.researchobject.org/ro-crate/1.1/data-entities.html#directories-on-the-web-dataset-distributions + */ + @Test + void testDirWithHasPartDeserialization() throws IOException { + String id = "second_content"; + DataEntity second_content = new DataEntity.DataEntityBuilder() + .setLocationWithExceptions(Paths.get("does_not_matter")) + .setId(id) + .addProperty( + "description", + "This entity just describes one of the contents in the Dir" + ) + .build(); + + // we can add to hasPart using directly the id, or passing the entity to it + DataSetEntity dir = new DataSetEntity.DataSetBuilder() + .setLocation(URI.create("https://www.example.com/urltodir")) + .addProperty("name", "Directory that is located on the web") + .addToHasPart("first_content") + .addToHasPart(second_content) + .build(); + + assertTrue(dir.hasPart(id)); + HelpFunctions.compareEntityWithFile( + dir, + "/json/entities/data/directoryWeb.json" + ); + } + + @Test + void testWebDir() { + JsonObject propertyValue = Json.objectNode(); + propertyValue.put( + "@id", + "http://example.com/downloads/2020/lots_of_little_files.zip" + ); + + DataSetEntity dir = new DataSetEntity.DataSetBuilder() + .setLocationWithExceptions(Paths.get("lots_of_little_files/")) + .setId("lots_of_little_files/") + .addProperty("name", "Too many files") + .addProperty( + "description", + "This directory contains many small files, that we're not going to describe in detail." + ) + .addProperty("distribution", propertyValue) + .build(); + + assertEquals(dir.getProperty("distribution"), propertyValue); + + DataEntity webDir = new DataEntity.DataEntityBuilder() + .setLocation( + URI.create("http://example.com/downloads/2020/lots_of_little_files.zip") + ) + .addType("DataDownload") + .addProperty("encodingFormat", "application/zip") + .addProperty("contentSize", "82818928") + .build(); + + assertNotNull(webDir); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/FileEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/FileEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/data/FileEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/FileEntityTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java new file mode 100644 index 00000000..a16aae6c --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java @@ -0,0 +1,81 @@ +package edu.kit.datamanager.ro_crate.entities.data; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.json.Json; +import java.io.IOException; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +/** + * @author Nikola Tzotchev on 5.2.2022 г. + * @version 1 + */ +public class RootDataEntityTest { + + @Test + void testSerialization() throws IOException { + String id1 = "file1_id"; + FileEntity file1 = new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(Paths.get("does_not_matter")) + .setId(id1) + .build(); + + String id2 = "file2_id"; + FileEntity file2 = new FileEntity.FileEntityBuilder() + .addIdProperty("name", "dsklfajs") + .addIdProperty("name", "11111111") + .setLocationWithExceptions(Paths.get("does_not_matter")) + .setId(id2) + .build(); + + RootDataEntity rootDataEntity = new RootDataEntity.RootDataEntityBuilder() + .addProperty("identifier", "https://doi.org/10.4225/59/59672c09f4a4b") + .addProperty("datePublished", "2000-02-01T00:00:00Z") + .addProperty( + "name", + "Data files associated with the manuscript:Effects of facilitated family case conferencing for ..." + ) + .addProperty( + "description", + "Palliative care planning for nursing home residents with advanced dementia ..." + ) + .addToHasPart(file1) + .addToHasPart(file2) + .addAuthor("a1") + .addAuthor("a2") + .build(); + + assertTrue(rootDataEntity.hasPart(id1)); + assertTrue(rootDataEntity.hasPart(id2)); + HelpFunctions.compareEntityWithFile( + rootDataEntity, + "/json/entities/data/root.json" + ); + } + + @Test + void testSerializationMinimalExample() + throws IOException, IllegalArgumentException { + RootDataEntity rootDataEntity = new RootDataEntity.RootDataEntityBuilder() + .addProperty("name", "Data files") + .addProperty("description", "Palliative care planning...") + .addDateTimePropertyWithExceptions( + "datePublished", + "2024-02-09T08:21:41Z" + ) + .addProperty( + "license", + Json.objectNode().put( + "@id", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + ) + .build(); + HelpFunctions.compareEntityWithFile( + rootDataEntity, + "/json/entities/data/rootMinimalExample.json" + ); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntityTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java similarity index 58% rename from src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java index 8ab685fa..ca8451b1 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java @@ -1,78 +1,81 @@ package edu.kit.datamanager.ro_crate.externalproviders; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.junit.jupiter.api.Assertions.assertTrue; import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.externalproviders.dataentities.ImportFromDataCite; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; import edu.kit.datamanager.ro_crate.validation.Validator; - +import java.io.IOException; +import java.util.Optional; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertTrue; - class DataCiteImportTest { @Test void dataCiteImportNewCrate() { - var crate = ImportFromDataCite - .createCrateFromDataCiteResource( - "https://api.datacite.org/application/vnd.datacite.datacite+json/10.1594/pangaea.149669", - Optional.of("importedCrate"), - Optional.of("description"), - Optional.of("2024"), - Optional.of("https://creativecommons.org/licenses/by-nc-sa/3.0/au/")); + var crate = ImportFromDataCite.createCrateFromDataCiteResource( + "https://api.datacite.org/application/vnd.datacite.datacite+json/10.1594/pangaea.149669", + Optional.of("importedCrate"), + Optional.of("description"), + Optional.of("2024"), + Optional.of("https://creativecommons.org/licenses/by-nc-sa/3.0/au/") + ); Validator validator = new Validator(new JsonSchemaValidation()); assertTrue(validator.validate(crate)); } @Test void dataCiteImportToExistingCrate() { - var crate = new RoCrate.RoCrateBuilder("name", "description", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/").build(); - ImportFromDataCite.addDataCiteToCrate("https://api.datacite.org/application/vnd.datacite.datacite+json/10.1594/pangaea.149669", crate); + var crate = new RoCrate.RoCrateBuilder( + "name", + "description", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ).build(); + ImportFromDataCite.addDataCiteToCrate( + "https://api.datacite.org/application/vnd.datacite.datacite+json/10.1594/pangaea.149669", + crate + ); Validator validator = new Validator(new JsonSchemaValidation()); assertTrue(validator.validate(crate)); } @Test void dataCiteImportFromExistingJson() { - String url = "https://api.datacite.org/application/vnd.datacite.datacite+json/10.5061/dryad.91741"; + String url = + "https://api.datacite.org/application/vnd.datacite.datacite+json/10.5061/dryad.91741"; CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet request = new HttpGet(url); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode jsonNode; + JsonValue jsonNode; try { CloseableHttpResponse response = httpClient.execute(request); - jsonNode = objectMapper.readValue(response.getEntity().getContent(), - JsonNode.class); + jsonNode = Json.parse(response.getEntity().getContent()); var crate = ImportFromDataCite.createCrateFromDataCiteJson( jsonNode, Optional.of("importedCrate"), Optional.of("description"), Optional.of("2024"), - Optional.of("https://creativecommons.org/licenses/by-nc-sa/3.0/au/")); + Optional.of("https://creativecommons.org/licenses/by-nc-sa/3.0/au/") + ); Validator validator = new Validator(new JsonSchemaValidation()); assertTrue(validator.validate(crate)); - String anotherUrl = "https://api.datacite.org/application/vnd.datacite.datacite+json/10.5061/dryad.dr554m2"; + String anotherUrl = + "https://api.datacite.org/application/vnd.datacite.datacite+json/10.5061/dryad.dr554m2"; request = new HttpGet(anotherUrl); response = httpClient.execute(request); - jsonNode = objectMapper.readValue(response.getEntity().getContent(), - JsonNode.class); + jsonNode = Json.parse(response.getEntity().getContent()); assertTrue(validator.validate(crate)); ImportFromDataCite.addDataCiteToCrateFromJson(jsonNode, crate); - } catch (IOException e) { e.printStackTrace(); } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/OrcidProviderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/OrcidProviderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/externalproviders/OrcidProviderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/OrcidProviderTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java new file mode 100644 index 00000000..f26861e0 --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java @@ -0,0 +1,53 @@ +package edu.kit.datamanager.ro_crate.externalproviders; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import edu.kit.datamanager.ro_crate.entities.contextual.OrganizationEntity; +import edu.kit.datamanager.ro_crate.externalproviders.organizationprovider.RorProvider; +import java.io.IOException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @author Nikola Tzotchev on 11.2.2022 г. + * @version 1 + */ +public class RorProviderTest { + + @Test + void testExternalRorProvider() throws IOException { + OrganizationEntity organizationEntity = RorProvider.getOrganization( + "https://ror.org/04t3en479" + ); + assertNotNull(organizationEntity); + Assertions.assertEquals( + "https://ror.org/04t3en479", + organizationEntity.getProperty("@id").asText() + ); + Assertions.assertEquals( + "https://ror.org/04t3en479", + organizationEntity.getProperty("url").asText() + ); + Assertions.assertEquals( + "Karlsruhe Institute of Technology", + organizationEntity.getProperty("name").asText() + ); + } + + @Test + void testInvalidRorUrl() { + assertThrows(IllegalArgumentException.class, () -> { + RorProvider.getOrganization("https://notror.org/04t3en479"); + }); + } + + @Test + void testInvalidRorId() { + OrganizationEntity organizationEntity = RorProvider.getOrganization( + "https://ror.org/42" + ); + assertNull(organizationEntity); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/ZenodoImportTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/ZenodoImportTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/externalproviders/ZenodoImportTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/ZenodoImportTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/json/JsonDiscoveryTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/json/JsonDiscoveryTest.java new file mode 100644 index 00000000..5f2a6a1c --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/json/JsonDiscoveryTest.java @@ -0,0 +1,96 @@ +package edu.kit.datamanager.ro_crate.json; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * The discovery failures must tell a user who does not know the module layout + * exactly what to add or exclude, so the messages must name the backend + * artifacts. + */ +class JsonDiscoveryTest { + + private static JsonAdapter stubAdapter() { + return new JsonAdapter() { + @Override + public JsonObject objectNode() { + return null; + } + + @Override + public JsonArray arrayNode() { + return null; + } + + @Override + public JsonValue parse(String json) { + return null; + } + + @Override + public JsonValue parse(File file) { + return null; + } + + @Override + public JsonValue parse(InputStream stream) { + return null; + } + + @Override + public String write(JsonValue value) { + return null; + } + + @Override + public String writePretty(JsonValue value) { + return null; + } + }; + } + + @Test + void missingBackendMessageNamesTheBackendArtifacts() { + var exception = assertThrows( + IllegalStateException.class, + () -> Json.uniqueCandidate( + Collections.emptyIterator(), + JsonAdapter.class + ) + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java"), + "message should name the Jackson 2 artifact: " + exception.getMessage() + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java-jackson3"), + "message should name the Jackson 3 artifact: " + exception.getMessage() + ); + } + + @Test + void multipleBackendsMessageSuggestsTheFix() { + JsonAdapter adapter = stubAdapter(); + var exception = assertThrows( + IllegalStateException.class, + () -> Json.uniqueCandidate( + List.of(adapter, adapter).iterator(), + JsonAdapter.class + ) + ); + assertTrue( + exception.getMessage().contains("exclude"), + "message should suggest excluding one backend: " + exception.getMessage() + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java-jackson3"), + "message should name the Jackson 3 artifact: " + exception.getMessage() + ); + } +} diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java new file mode 100644 index 00000000..e650d1eb --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java @@ -0,0 +1,153 @@ +package edu.kit.datamanager.ro_crate.json; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** Unit tests for the neutral, Jackson-free JSON tree model. */ +class NeutralJsonModelTest { + + @Test + void objectPreservesInsertionOrderAndOverwritesInPlace() { + JsonObject object = JsonObject.create(); + object.put("b", "second").put("a", "first"); + assertEquals("b", object.names().get(0)); + object.put("b", "changed"); + assertEquals(2, object.size()); + assertEquals("changed", object.get("b").asText()); + } + + @Test + void accessorsMirrorJacksonSemantics() { + JsonObject object = JsonObject.create().put("name", "crate"); + assertNull(object.get("absent")); + assertTrue(object.path("absent").isMissing()); + assertEquals("", object.path("absent").asText()); + assertEquals("DEF", object.path("absent").asText("DEF")); + assertTrue(object.has("name")); + assertFalse(object.has("absent")); + assertEquals("crate", object.get("name").asText()); + assertEquals("", object.asText()); // containers mirror Jackson: "" + } + + @Test + void putNullStoresJsonNull() { + JsonObject object = JsonObject.create().putNull("x"); + assertTrue(object.get("x").isNull()); + } + + @Test + void removeWorksByIndexAndName() { + JsonArray array = JsonArray.create().add("a").add("b").add("c"); + assertEquals(new JsonString("b"), array.remove(1)); + assertEquals(2, array.size()); + + JsonObject object = JsonObject.create().put("x", 1).put("y", 2); + assertEquals(new JsonNumber(2), object.remove("y")); + assertNull(object.get("y")); + } + + @Test + void numbersKeepTheirJavaType() throws IOException { + JsonValue parsed = Json.parse("{\"i\": 1, \"l\": 2147483648, \"d\": 1.5}"); + JsonObject object = (JsonObject) parsed; + assertTrue(object.get("i").isNumber()); + assertEquals(1, ((JsonNumber) object.get("i")).getValue()); + assertEquals(2147483648L, ((JsonNumber) object.get("l")).getValue()); + assertEquals(1.5, ((JsonNumber) object.get("d")).getValue()); + } + + @Test + void equalityIsStructuralAndTypeSensitiveForNumbers() { + JsonObject first = JsonObject.create().put("a", JsonArray.create().add(1)); + JsonObject second = JsonObject.create().put("a", JsonArray.create().add(1)); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + + // Jackson numeric node equality is class-strict (verified against + // Jackson 2.22.1: IntNode(1) != LongNode(1), != DoubleNode(1.0)) + assertNotEquals(new JsonNumber(1), new JsonNumber(1.0)); + assertNotEquals(new JsonNumber(1), new JsonNumber(1L)); + assertNotEquals(new JsonNumber(1), new JsonNumber(2147483648L)); + assertNotEquals(new JsonNumber(1), new JsonString("1")); + + // object equality is order-insensitive, mirroring Jackson's LinkedHashMap + JsonObject ordered = JsonObject.create().put("a", 1).put("b", 2); + JsonObject reordered = JsonObject.create().put("b", 2).put("a", 1); + assertEquals(ordered, reordered); + assertEquals(ordered.hashCode(), reordered.hashCode()); + } + + @Test + void deepCopyIsIndependent() { + JsonObject original = JsonObject.create().put( + "nested", + JsonObject.create().put("x", 1) + ); + JsonObject copy = original.deepCopy(); + ((JsonObject) copy.get("nested")).put("x", 2); + assertEquals( + 1, + ((JsonNumber) original.path("nested").path("x")).getValue() + ); + assertEquals(original, original.deepCopy()); + } + + @Test + void valueNodeSemanticsMirrorJackson() { + assertTrue(new JsonString("x").isValueNode()); + assertTrue(JsonNull.INSTANCE.isValueNode()); + assertTrue(JsonNull.INSTANCE.isNull()); + assertFalse(JsonNull.INSTANCE.isString()); + assertEquals("null", JsonNull.INSTANCE.asText()); + assertEquals("default", JsonNull.INSTANCE.asText("default")); + assertEquals("true", JsonBoolean.of(true).asText()); + assertSame(JsonBoolean.TRUE, JsonBoolean.of(true)); + assertTrue(JsonMissing.INSTANCE.isMissing()); + } + + @Test + void setAllCopiesInOrder() { + JsonObject source = JsonObject.create().put("a", 1).put("b", 2); + JsonObject target = JsonObject.create().put("b", 9).put("c", 3); + target.setAll(source); + assertEquals(3, target.size()); + assertEquals(new JsonNumber(2), target.get("b")); + assertEquals(new JsonNumber(1), target.get("a")); + assertEquals(new JsonNumber(3), target.get("c")); + } + + @Test + void fieldsAndIterationExposeValues() { + JsonObject object = JsonObject.create().put("x", "1").put("y", "2"); + var iterator = object.fields(); + assertEquals("x", iterator.next().getKey()); + assertEquals("y", iterator.next().getKey()); + assertFalse(iterator.hasNext()); + + int count = 0; + for (JsonValue value : object) { + count++; + assertTrue(value.isString()); + } + assertEquals(2, count); + } + + @Test + void writeProducesCompactJsonViaAdapter() throws IOException { + JsonObject object = JsonObject.create() + .put("name", "crate") + .put("count", 3) + .putNull("missing"); + assertEquals( + "{\"name\":\"crate\",\"count\":3,\"missing\":null}", + Json.write(object) + ); + } +} diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java new file mode 100644 index 00000000..3640e735 --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java @@ -0,0 +1,82 @@ +package edu.kit.datamanager.ro_crate.other; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; +import io.json.compare.JSONCompare; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +public class UtilFunctionsTest { + + @Test + void testUnwrapSimple() throws IOException { + InputStream fileJson = UtilFunctionsTest.class.getResourceAsStream( + "/json/unwrap/simple.json" + ); + + JsonValue node = JsonUtilFunctions.unwrapSingleArray(Json.parse(fileJson)); + JsonValue expectedResult = Json.parse( + UtilFunctionsTest.class.getResourceAsStream( + "/json/unwrap/simpleResult.json" + ) + ); + JSONCompare.assertMatches(node.toString(), expectedResult.toString()); + } + + @Test + void testGettingIdEntriesFromJsonProperty() { + JsonObject jsonNode = Json.objectNode(); + jsonNode.put("@id", "string1"); + + var result = JsonUtilFunctions.getIdPropertiesFromProperty(jsonNode); + assertEquals(1, result.size()); + assertEquals("string1", result.stream().findFirst().get()); + + JsonArray arrayNode = Json.arrayNode(); + arrayNode.add(Json.objectNode().put("@id", "string1")); + arrayNode.add(Json.objectNode().put("@id", "string2")); + + result = JsonUtilFunctions.getIdPropertiesFromProperty(arrayNode); + assertEquals(2, result.size()); + assertTrue(result.contains("string1")); + assertTrue(result.contains("string2")); + } + + @Test + void testGettingIdFromObject() { + JsonObject jsonNode = Json.objectNode(); + + jsonNode.put("random", "not id"); + jsonNode.set("other", Json.arrayNode().add("kdfjdlk").add("kdfjalkfj")); + + jsonNode.set("test", Json.objectNode().put("@id", "test1")); + + jsonNode.set("another", Json.objectNode().put("@id", "test2")); + + jsonNode.set( + "singleArray", + Json.arrayNode().add(Json.objectNode().put("@id", "test3")) + ); + jsonNode.set( + "multipleArrayEl", + Json.arrayNode() + .add(Json.objectNode().put("@id", "test4")) + .add(Json.objectNode().put("@id", "test5")) + ); + + var set = JsonUtilFunctions.getIdPropertiesFromJsonNode(jsonNode); + + assertEquals(5, set.size()); + + for (int i = 1; i <= 5; i++) { + assertTrue(set.contains("test" + i)); + } + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/payload/PayloadTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/payload/PayloadTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/payload/PayloadTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/payload/PayloadTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/preview/PreviewTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/preview/PreviewTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/preview/PreviewTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/preview/PreviewTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ElnFileFormatTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/ElnFileFormatTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/ElnFileFormatTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/ElnFileFormatTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java new file mode 100644 index 00000000..00b0289f --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java @@ -0,0 +1,48 @@ +package edu.kit.datamanager.ro_crate.reader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.Crate; +import java.io.IOException; +import java.util.stream.StreamSupport; +import org.junit.jupiter.api.Test; + +/** + * These reading tests are specific for reading 1.2 compliant crates. + * + * They will refer to 1.2-DRAFT as long as the specification is not final. + * Base for these tests are usually examples from the specification, but may + * contain modifications if no example for a certain property is given. + * + * Current specification: https://www.researchobject.org/ro-crate/1.2-DRAFT/ + */ +public class RoCrateReaderSpec12Test { + + /** + * Test reading a minimal crate with multiple conformsTo values, as described in + * https://www.researchobject.org/ro-crate/1.2-DRAFT/profiles.html#declaring-conformance-of-an-ro-crate-profile + */ + @Test + void testReadingCrateWithConformsToArray() throws IOException { + String path = this.getClass() + .getResource("/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array") + .getPath(); + Crate crate = Readers.newFolderReader().readCrate(path); + JsonValue conformsTo = crate.getJsonDescriptor().getProperty("conformsTo"); + assertTrue(conformsTo.isArray()); + assertEquals(2, conformsTo.size()); + // filter the list to only contain the two expected values and expect no changes + long numFiltered = StreamSupport.stream(conformsTo.spliterator(), false) + .filter(node -> { + String asText = node.path("@id").asText(); + return ( + asText.equals("https://w3id.org/ro/crate/1.2-DRAFT") || + asText.equals("https://example.com/my/profile/1.0-TEST") + ); + }) + .count(); + assertEquals(2, numFiltered); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/TestableReaderStrategy.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/TestableReaderStrategy.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/TestableReaderStrategy.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/TestableReaderStrategy.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/special/CrateVersionTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/special/CrateVersionTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/special/CrateVersionTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/special/CrateVersionTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/special/IdentifierUtilsTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/special/IdentifierUtilsTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/special/IdentifierUtilsTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/special/IdentifierUtilsTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/util/FileSystemUtilTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/util/FileSystemUtilTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/util/FileSystemUtilTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/util/FileSystemUtilTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/validation/ValidationDiscoveryTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/validation/ValidationDiscoveryTest.java new file mode 100644 index 00000000..e339c802 --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/validation/ValidationDiscoveryTest.java @@ -0,0 +1,55 @@ +package edu.kit.datamanager.ro_crate.validation; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * The discovery failures must tell a user who does not know the module layout + * exactly what to add or exclude, so the messages must name the backend + * artifacts. + */ +class ValidationDiscoveryTest { + + @Test + void missingBackendMessageNamesTheBackendArtifacts() { + var exception = assertThrows( + IllegalStateException.class, + () -> Validation.uniqueCandidate( + Collections.emptyIterator(), + ValidatorStrategy.class + ) + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java"), + "message should name the Jackson 2 artifact: " + exception.getMessage() + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java-jackson3"), + "message should name the Jackson 3 artifact: " + exception.getMessage() + ); + } + + @Test + void multipleBackendsMessageSuggestsTheFix() { + ValidatorStrategy strategy = crate -> true; + var exception = assertThrows( + IllegalStateException.class, + () -> Validation.uniqueCandidate( + List.of(strategy, strategy).iterator(), + ValidatorStrategy.class + ) + ); + assertTrue( + exception.getMessage().contains("exclude"), + "message should suggest excluding one backend: " + exception.getMessage() + ); + assertTrue( + exception.getMessage().contains("edu.kit.datamanager:ro-crate-java-jackson3"), + "message should name the Jackson 3 artifact: " + exception.getMessage() + ); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ElnFileWriterTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/ElnFileWriterTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/ElnFileWriterTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/ElnFileWriterTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java diff --git a/core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java new file mode 100644 index 00000000..7d8a877e --- /dev/null +++ b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -0,0 +1,558 @@ +package edu.kit.datamanager.ro_crate.writer; + +import static edu.kit.datamanager.ro_crate.util.Graph.*; +import static org.junit.jupiter.api.Assertions.*; + +import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class RoCrateMetadataGenerationTest { + + private final String currentVersionId = + new ProvenanceManager().getLibraryId(); + + 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); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue graph = rootNode.get("@graph"); + + // Find ro-crate-java entity + JsonValue 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" + ); + + // Find CreateAction entity + JsonValue createActionEntity = findEntityByType(graph, "CreateAction"); + assertNotNull(createActionEntity, "CreateAction entity should exist"); + assertNotNull( + createActionEntity.get("startTime"), + "CreateAction should have startTime" + ); + assertEquals( + this.currentVersionId, + 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(); + 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); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue roCrateJavaEntity = findEntityById( + rootNode.get("@graph"), + this.currentVersionId + ); + + 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" + ); + } + + @Test + 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); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue graph = rootNode.get("@graph"); + + // Get both entities + JsonValue roCrateJavaEntity = findEntityById(graph, this.currentVersionId); + JsonValue createActionEntity = findEntityByType(graph, "CreateAction"); + + assertNotNull(roCrateJavaEntity, "ro-crate-java entity should exist"); + assertNotNull(createActionEntity, "CreateAction entity should exist"); + + // Test CreateAction -> ro-crate-java reference + JsonValue agentRef = createActionEntity.get("agent"); + assertNotNull(agentRef, "CreateAction should have agent property"); + assertEquals( + this.currentVersionId, + agentRef.get("@id").asText(), + "CreateAction's agent should reference ro-crate-java" + ); + + // Test ro-crate-java -> CreateAction reference + JsonValue 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" + ); + } + + @Test + 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); + + 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") + ); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue graph = rootNode.get("@graph"); + + // Get ro-crate-java entity + JsonValue roCrateJavaEntity = findEntityById(graph, this.currentVersionId); + 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 + JsonValue createAction = findEntityByType(graph, "CreateAction"); + assertNotNull(createAction, "should have one CreateAction"); + + JsonValue[] createActions = findEntitiesByType(graph, "CreateAction"); + assertEquals( + 1, + createActions.length, + "should have exactly one CreateAction" + ); + + JsonValue[] updateActions = findEntitiesByType(graph, "UpdateAction"); + assertEquals(2, updateActions.length, "should have two UpdateActions"); + + // Verify CreateAction properties + assertNotNull( + createAction.get("startTime"), + "CreateAction should have startTime" + ); + assertEquals( + this.currentVersionId, + createAction.get("agent").get("@id").asText(), + "CreateAction should reference ro-crate-java as agent" + ); + + // Verify UpdateAction properties + for (JsonValue updateAction : updateActions) { + assertNotNull( + updateAction.get("startTime"), + "UpdateAction should have startTime" + ); + assertEquals( + this.currentVersionId, + 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(); + // 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( + createTime.compareTo(updateTime2) < 0, + "Second update should be after creation" + ); + } + + @Test + 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") + ); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue roCrateJavaEntity = findEntityById( + rootNode.get("@graph"), + this.currentVersionId + ); + + // 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(); + 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") + ); + HelpFunctions.prettyPrintJsonString(metadata); + + // Parse metadata file + JsonValue rootNode = Json.parse(metadata); + JsonValue roCrateJavaEntity = findEntityById( + rootNode.get("@graph"), + this.currentVersionId + ); + + // 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" + ); + } + + @Test + 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) + Writers.newFolderWriter() + .withAutomaticProvenance(null) + .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); + + JsonValue originalRoot = Json.parse(originalMetadata); + JsonValue originalGraph = originalRoot.get("@graph"); + assertNull( + findEntityById(originalGraph, this.currentVersionId), + "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() + ); + validateCrate(modifiedCrate); + modifiedCrate + .getRootDataEntity() + .addProperty("description", "Modified crate"); + validateCrate(modifiedCrate); + + // 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); + + JsonValue modifiedRoot = Json.parse(modifiedMetadata); + JsonValue modifiedGraph = modifiedRoot.get("@graph"); + + // Verify ro-crate-java entity was added + JsonValue roCrateJavaEntity = findEntityById( + modifiedGraph, + this.currentVersionId + ); + 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" + ); + + JsonValue updateAction = findEntityByType(modifiedGraph, "UpdateAction"); + assertNotNull(updateAction, "Should have UpdateAction"); + + // Verify update action properties + assertNotNull( + updateAction.get("startTime"), + "UpdateAction should have startTime" + ); + assertEquals( + this.currentVersionId, + 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").isObject(), + "ro-crate-java should have a single reference to an UpdateAction" + ); + assertEquals( + updateAction.get("@id").asText(), + roCrateJavaEntity.get("Action").get("@id").asText(), + "ro-crate-java should reference the UpdateAction" + ); + } + + @Test + 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()); + Thread.sleep(10); + + // 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()); + + // Read and verify the metadata + String metadata = Files.readString( + outputPath.resolve("ro-crate-metadata.json") + ); + HelpFunctions.prettyPrintJsonString(metadata); + + JsonValue root = Json.parse(metadata); + JsonValue graph = root.get("@graph"); + + // Should have both CreateAction and UpdateAction + JsonValue createAction = findEntityByType(graph, "CreateAction"); + assertNotNull(createAction, "Original CreateAction should be preserved"); + + JsonValue[] 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 + JsonValue roCrateJavaEntity = findEntityById(graph, this.currentVersionId); + //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" + ); + } +} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/TestableWriterStrategy.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/TestableWriterStrategy.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/TestableWriterStrategy.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/TestableWriterStrategy.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java diff --git a/src/test/resources/crates/extendedContextExample/ro-crate-metadata.json b/core/src/test/resources/crates/extendedContextExample/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/extendedContextExample/ro-crate-metadata.json rename to core/src/test/resources/crates/extendedContextExample/ro-crate-metadata.json diff --git a/src/test/resources/crates/hasPartHierarchy/README.md b/core/src/test/resources/crates/hasPartHierarchy/README.md similarity index 100% rename from src/test/resources/crates/hasPartHierarchy/README.md rename to core/src/test/resources/crates/hasPartHierarchy/README.md diff --git a/src/test/resources/crates/hasPartHierarchy/ro-crate-metadata.json b/core/src/test/resources/crates/hasPartHierarchy/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/hasPartHierarchy/ro-crate-metadata.json rename to core/src/test/resources/crates/hasPartHierarchy/ro-crate-metadata.json diff --git a/src/test/resources/crates/other/idrc_project/ro-crate-metadata.json b/core/src/test/resources/crates/other/idrc_project/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/other/idrc_project/ro-crate-metadata.json rename to core/src/test/resources/crates/other/idrc_project/ro-crate-metadata.json diff --git a/src/test/resources/crates/simple_crate/ro-crate-metadata.json b/core/src/test/resources/crates/simple_crate/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/simple_crate/ro-crate-metadata.json rename to core/src/test/resources/crates/simple_crate/ro-crate-metadata.json diff --git a/src/test/resources/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array/ro-crate-metadata.json b/core/src/test/resources/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array/ro-crate-metadata.json rename to core/src/test/resources/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array/ro-crate-metadata.json diff --git a/src/test/resources/crates/validation/workflowschema.json b/core/src/test/resources/crates/validation/workflowschema.json similarity index 100% rename from src/test/resources/crates/validation/workflowschema.json rename to core/src/test/resources/crates/validation/workflowschema.json diff --git a/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip b/core/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip similarity index 100% rename from src/test/resources/crates/workflowhub/workflow-109-5.crate.zip rename to core/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip diff --git a/src/test/resources/crates/workflowhub/workflow1/CHANGELOG.md b/core/src/test/resources/crates/workflowhub/workflow1/CHANGELOG.md similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/CHANGELOG.md rename to core/src/test/resources/crates/workflowhub/workflow1/CHANGELOG.md diff --git a/src/test/resources/crates/workflowhub/workflow1/README.md b/core/src/test/resources/crates/workflowhub/workflow1/README.md similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/README.md rename to core/src/test/resources/crates/workflowhub/workflow1/README.md diff --git a/src/test/resources/crates/workflowhub/workflow1/ro-crate-metadata.json b/core/src/test/resources/crates/workflowhub/workflow1/ro-crate-metadata.json similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/ro-crate-metadata.json rename to core/src/test/resources/crates/workflowhub/workflow1/ro-crate-metadata.json diff --git a/src/test/resources/crates/workflowhub/workflow1/ro-crate-preview.html b/core/src/test/resources/crates/workflowhub/workflow1/ro-crate-preview.html similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/ro-crate-preview.html rename to core/src/test/resources/crates/workflowhub/workflow1/ro-crate-preview.html diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/by_variant_report.tsv b/core/src/test/resources/crates/workflowhub/workflow1/test-data/by_variant_report.tsv similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/by_variant_report.tsv rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/by_variant_report.tsv diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/combined_variant_report.tsv b/core/src/test/resources/crates/workflowhub/workflow1/test-data/combined_variant_report.tsv similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/combined_variant_report.tsv rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/combined_variant_report.tsv diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/ont-artic.vcf b/core/src/test/resources/crates/workflowhub/workflow1/test-data/ont-artic.vcf similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/ont-artic.vcf rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/ont-artic.vcf diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/pe-artic.vcf b/core/src/test/resources/crates/workflowhub/workflow1/test-data/pe-artic.vcf similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/pe-artic.vcf rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/pe-artic.vcf diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/pe-illumina-wgs.vcf b/core/src/test/resources/crates/workflowhub/workflow1/test-data/pe-illumina-wgs.vcf similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/pe-illumina-wgs.vcf rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/pe-illumina-wgs.vcf diff --git a/src/test/resources/crates/workflowhub/workflow1/test-data/se-illumina-wgs.vcf b/core/src/test/resources/crates/workflowhub/workflow1/test-data/se-illumina-wgs.vcf similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/test-data/se-illumina-wgs.vcf rename to core/src/test/resources/crates/workflowhub/workflow1/test-data/se-illumina-wgs.vcf diff --git a/src/test/resources/crates/workflowhub/workflow1/variation-reporting-test.yml b/core/src/test/resources/crates/workflowhub/workflow1/variation-reporting-test.yml similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/variation-reporting-test.yml rename to core/src/test/resources/crates/workflowhub/workflow1/variation-reporting-test.yml diff --git a/src/test/resources/crates/workflowhub/workflow1/variation-reporting.ga b/core/src/test/resources/crates/workflowhub/workflow1/variation-reporting.ga similarity index 100% rename from src/test/resources/crates/workflowhub/workflow1/variation-reporting.ga rename to core/src/test/resources/crates/workflowhub/workflow1/variation-reporting.ga diff --git a/core/src/test/resources/golden/extendedContextExample.metadata.json b/core/src/test/resources/golden/extendedContextExample.metadata.json new file mode 100644 index 00000000..34e0d425 --- /dev/null +++ b/core/src/test/resources/golden/extendedContextExample.metadata.json @@ -0,0 +1 @@ +{"@context":["https://w3id.org/ro/crate/1.1/context",{"owl":"https://www.w3.org/TR/owl-ref/#","custom":"_:","datacite":"http://datacite.org/schema/kernel-4#","xsd":"http://www.w3.org/2001/XMLSchema#","rdfs":"https://www.w3.org/2000/01/rdf-schema#"}],"@graph":[{"@id":"./","@type":"Dataset","name":"minimal","description":"minimal RO_crate","datePublished":"2024","license":{"@id":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/"}},{"@type":"CreativeWork","@id":"ro-crate-metadata.json","conformsTo":{"@id":"https://w3id.org/ro/crate/1.1"},"about":{"@id":"./"}}]} \ No newline at end of file diff --git a/core/src/test/resources/golden/hasPartHierarchy.metadata.json b/core/src/test/resources/golden/hasPartHierarchy.metadata.json new file mode 100644 index 00000000..7e0634e7 --- /dev/null +++ b/core/src/test/resources/golden/hasPartHierarchy.metadata.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/ro/crate/1.1/context","@graph":[{"@id":"./","@type":"Dataset","name":"Root element","description":"Description","license":{"@id":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/"},"hasPart":[{"@id":"sub-b.file"},{"@id":"sub-a.file"}]},{"@id":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/","@type":"CreativeWork","description":"Contextual entity (license)","identifier":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/","name":"Attribution-NonCommercial-ShareAlike 3.0 Australia (CC BY-NC-SA 3.0 AU)"},{"@id":"looper-isPartOf.file","@type":"File","hasPart":{"@id":"sub-b.file"},"isPartOf":{"@id":"sub-b.file"}},{"@id":"looper.file","@type":"File","hasPart":{"@id":"sub-b.file"}},{"@type":"CreativeWork","@id":"ro-crate-metadata.json","conformsTo":{"@id":"https://w3id.org/ro/crate/1.1"},"about":{"@id":"./"}},{"@id":"sub-a.file","@type":"File"},{"@id":"sub-b-sub-a.file","@type":"File","hasPart":{"@id":"looper.file"}},{"@id":"sub-b-sub-b.file","@type":"File","isPartOf":{"@id":"sub-b.file"}},{"@id":"sub-b.file","@type":"File","hasPart":{"@id":"sub-b-sub-a.file"}}]} \ No newline at end of file diff --git a/core/src/test/resources/golden/idrc_project.metadata.json b/core/src/test/resources/golden/idrc_project.metadata.json new file mode 100644 index 00000000..885aa1fc --- /dev/null +++ b/core/src/test/resources/golden/idrc_project.metadata.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/ro/crate/1.1/context","@graph":[{"@id":"./","@type":"Dataset","contactPoint":{"@id":"cn@cameronneylon.net"},"path":"./","contributor":{"@id":"https://doi.org/10.3897/rio.2.e8880"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"datePublished":"2017","description":"Data relating to the IDRC funded project (described in https://doi.org/10.3897/rio.2.e8880) to examine data management policies and implementation for development funders. The project involved two parts: a review based on desk work and expert interviews and seven case studies of existing IDRC-funded projects. The case studies were supported by an Introductory Workshop in which the idea of data was examined and the issues involved in sharing discussed in detail. This was followed by an implementation phase in which the projects were supported in developing Data Management Plans. The performance against those plans was then assessed both by the participants and as part of the overall project to generate case studies that are to be published as part of the related RIO Journal Collection. The final project report will also be part of the same collection.","hasPart":[{"@id":"data/Introductory_Workshop_Presentations"},{"@id":"data/Final_Project_Workshop_Presentations"},{"@id":"data/SciDataCon Presentations"},{"@id":"data/Data Management Planning"},{"@id":"data/Introductory_Data_Workshop_Materials"},{"@id":"data/Project Codes.txt"},{"@id":"data/README.txt"},{"@id":"data/Final_Project_Workshop_Materials"},{"@id":"data/Policy and Implementation Review Interviews"}],"identifier":["https://dx.doi.org/10.5281/zenodo.844394","dx.doi.org/10.5281/zenodo.844394"],"name":"Dataset for IDRC Project: Exploring the opportunities and challenges of implementing open research strategies within development institutions","publisher":{"@id":"IDRC"},"relatedLink":["https://doi.org/10.3897/rio.3.e14673","https://riojournal.com/collection/18/"],"distribution":{"@id":"https://data.research.uts.edu.au/examples/ro-crate/0.2Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies/Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies.zip"}},{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190","@type":"Person","name":"Reem Wael"},{"@id":"9a4e89e1-13bf-4d44-b5f7-ced40eb33cb2","@type":"Person","name":"Constanza Figueroa"},{"@id":"a7b498c4-46fc-4e02-af5c-e72d4050eacb","@type":"Person","name":"Matthew Harvey"},{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605","@type":"Person","name":"Le Dang Trung"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8","@type":"Person","name":"Laura Foster"},{"@id":"bibo:interviewee","sameAs":"http://neologism.ecs.soton.ac.uk/bibo.html#interviewee","@type":"Thing"},{"@id":"bibo:interviewer","sameAs":"http://neologism.ecs.soton.ac.uk/bibo.html#interviewer","@type":"Thing"},{"@id":"cn@cameronneylon.net","@type":"ContactPoint","contactType":"customer service","email":"cn@cameronneylon.net","identifier":"cn@cameronneylon.net","name":"Contact Cameron Neylon"},{"@id":"cnam","@type":"Place","URL":"http://www.cnam.fr/","address":"292 Rue Saint-Martin, 75003 Paris, France","description":"French Conservatory of Arts and Crafts and location of the 2015 RDA Plenary in Paris","identifier":"cnam","name":"Conservatoire National des arts et métiers"},{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219","@type":"Person","name":"Danae Tapia"},{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa","@type":"Person","name":"Aïssa Diarra"},{"@id":"data/Data Management Planning","@type":"Dataset","contactPoint":"Cameron Neylon","path":"data/Data Management Planning","description":"Collection of files relating to the Pilot Project Data Management Planning process. Includes 1) Data Inventories collected at the first workshop or soon after where data expected to be generated from contributing projects was enumerated 2) Data Management Plans prepared during the course of the project (see also the published DMPs in RIO journal for the finalised version) 3) Notes of interviews with the contributing projects about their experience of the DMP process and 4) A self audit of data sharing against what was proposed in the Data Management Plans.","hasPart":[{"@id":"data/Data Management Planning/Data_Inventories"},{"@id":"data/Data Management Planning/Data_Management_Interviews"},{"@id":"data/Data Management Planning/Data_Management_Plans"},{"@id":"data/Data Management Planning/Final_Self_Audit"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments"}],"identifier":"./Data Management Planning","name":"Data Management Planning"},{"@id":"data/Data Management Planning/Data_Inventories","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Data Management Planning/Data_Inventories","description":"Each contributing project prepared a \"Data Inventory\" as part of the initial workshop. This involved a template for enumerating data products from the contributing project, potential issues with sharing and important characteristics of the data outputs.","hasPart":[{"@id":"data/Data Management Planning/Data_Inventories/BVH_107650_Virtual_Herbarium_CRIA_Data_inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/DED_107771 - Copyright User Rights - Data Inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/ECV_Data Audit Form-VN.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/IKC_107650_NaturalJustice_Data Inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/NDF_20160401_Problemes_negliges_systeme_sante_Afrique_Niger__data inventory_FR.doc"},{"@id":"data/Data Management Planning/Data_Inventories/TED_20160325-108098-africa-tobacco-data-inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/TED_20160406-108098-africa-tobacco-data-inventory.xlsx"}],"identifier":"./Data Management Planning/Data_Inventories","name":"Data Inventories"},{"@id":"data/Data Management Planning/Data_Inventories/BVH_107650_Virtual_Herbarium_CRIA_Data_inventory.xlsx","@type":"File","contentSize":"13800","path":"data/Data Management Planning/Data_Inventories/BVH_107650_Virtual_Herbarium_CRIA_Data_inventory.xlsx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8615-6652"},"creator":{"@id":"http://orcid.org/0000-0001-8615-6652"},"description":"Data Inventory for the Brazilian Virtual Herbarium Project","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"BVH_107650_Virtual_Herbarium_CRIA_Data_inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/DED_107771 - Copyright User Rights - Data Inventory.xlsx","@type":"File","contentSize":"4862","path":"data/Data Management Planning/Data_Inventories/DED_107771 - Copyright User Rights - Data Inventory.xlsx","copyrightHolder":{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219"},"creator":"Constanza Figuero","description":"Data Inventory for the Derechos Digitales project","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"DED_107771 - Copyright User Rights - Data Inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/ECV_Data Audit Form-VN.xlsx","@type":"File","contentSize":"29768","path":"data/Data Management Planning/Data_Inventories/ECV_Data Audit Form-VN.xlsx","copyrightHolder":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"creator":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"description":"Data Inventory for the Economic Committee of Vietnam project","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"ECV_Data Audit Form-VN.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/IKC_107650_NaturalJustice_Data Inventory.xlsx","@type":"File","contentSize":"15518","path":"data/Data Management Planning/Data_Inventories/IKC_107650_NaturalJustice_Data Inventory.xlsx","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"Data Inventory for the Indigenous Knowledge, Natural Justice project","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IKC_107650_NaturalJustice_Data Inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/NDF_20160401_Problemes_negliges_systeme_sante_Afrique_Niger__data inventory_FR.doc","@type":"File","contentSize":"84992","path":"data/Data Management Planning/Data_Inventories/NDF_20160401_Problemes_negliges_systeme_sante_Afrique_Niger__data inventory_FR.doc","copyrightHolder":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"creator":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"description":"Data Inventory for the Neglected Health Issues in Niger project","encodingFormat":"Microsoft Word Document","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/40","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"NDF_20160401_Problemes_negliges_systeme_sante_Afrique_Niger__data inventory_FR.doc"},{"@id":"data/Data Management Planning/Data_Inventories/TED_20160325-108098-africa-tobacco-data-inventory.xlsx","@type":"File","contentSize":"14086","path":"data/Data Management Planning/Data_Inventories/TED_20160325-108098-africa-tobacco-data-inventory.xlsx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Data Inventory for the Tobacco Economics Data project v1","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"TED_20160325-108098-africa-tobacco-data-inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Inventories/TED_20160406-108098-africa-tobacco-data-inventory.xlsx","@type":"File","contentSize":"16887","path":"data/Data Management Planning/Data_Inventories/TED_20160406-108098-africa-tobacco-data-inventory.xlsx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Data Inventory for the Tobacco Economics Data project v2","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"TED_20160406-108098-africa-tobacco-data-inventory.xlsx"},{"@id":"data/Data Management Planning/Data_Management_Interviews","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Data Management Planning/Data_Management_Interviews","description":"Notes of interviews that were taken with contributing project representatives during or after the process of Data Management Planning. The interviews focussed on which aspects of the data planning processes were useful or not. The interview prompt is also included.","hasPart":[{"@id":"data/Data Management Planning/Data_Management_Interviews/AAA_CN20160610_DiscussionPlan.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/BVH_CN20160609_Dora_Cahnos_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/DED_CN20160609_DanaeTapia_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/ECV_CN20160608-Le_Dung_Trang_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/HMP_CN20160602_Reem_Wael_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/IKC_CN20160608_Cath_Traynor_&_Laura_Foster_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/NDF_CN20160610_AissaDiarra_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/TED_CN20160602_Lynn_Woolfrey_Note_of_discussion.rtf"}],"identifier":"./Data Management Planning/Data_Management_Interviews","name":"Data management interviews"},{"@id":"data/Data Management Planning/Data_Management_Interviews/AAA_CN20160610_DiscussionPlan.rtf","@type":"File","contentSize":"70398","path":"data/Data Management Planning/Data_Management_Interviews/AAA_CN20160610_DiscussionPlan.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Discussion plan and questions for the interviews","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/AAA_CN20160610_DiscussionPlan.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/BVH_CN20160609_Dora_Cahnos_Note_of_discussion.rtf","@type":"File","contentSize":"155543","path":"data/Data Management Planning/Data_Management_Interviews/BVH_CN20160609_Dora_Cahnos_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Dora Canhos, BVH project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/BVH_CN20160609_Dora_Cahnos_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/DED_CN20160609_DanaeTapia_Note_of_discussion.rtf","@type":"File","contentSize":"14656","path":"data/Data Management Planning/Data_Management_Interviews/DED_CN20160609_DanaeTapia_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Danae Tapia, DED project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/50","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/DED_CN20160609_DanaeTapia_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/ECV_CN20160608-Le_Dung_Trang_Note_of_discussion.rtf","@type":"File","contentSize":"11919","path":"data/Data Management Planning/Data_Management_Interviews/ECV_CN20160608-Le_Dung_Trang_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Le Dang Trung, ECV project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/50","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/ECV_CN20160608-Le_Dung_Trang_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/HMP_CN20160602_Reem_Wael_Note_of_discussion.rtf","@type":"File","contentSize":"150840","path":"data/Data Management Planning/Data_Management_Interviews/HMP_CN20160602_Reem_Wael_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Reem Wael, HMP project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/HMP_CN20160602_Reem_Wael_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/IKC_CN20160608_Cath_Traynor_&_Laura_Foster_Note_of_discussion.rtf","@type":"File","contentSize":"216674","path":"data/Data Management Planning/Data_Management_Interviews/IKC_CN20160608_Cath_Traynor_&_Laura_Foster_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Cath Traynor, Laura Foster, IKC project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/IKC_CN20160608_Cath_Traynor_&_Laura_Foster_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/NDF_CN20160610_AissaDiarra_Note_of_discussion.rtf","@type":"File","contentSize":"170014","path":"data/Data Management Planning/Data_Management_Interviews/NDF_CN20160610_AissaDiarra_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Aïssa Diarra, NDF project and Pascal Aventurier, mentor and translator","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/NDF_CN20160610_AissaDiarra_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Interviews/TED_CN20160602_Lynn_Woolfrey_Note_of_discussion.rtf","@type":"File","contentSize":"214894","path":"data/Data Management Planning/Data_Management_Interviews/TED_CN20160602_Lynn_Woolfrey_Note_of_discussion.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Note of interview with Lynn Woolfrey, TED project","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Data_Management_Interviews/TED_CN20160602_Lynn_Woolfrey_Note_of_discussion.rtf"},{"@id":"data/Data Management Planning/Data_Management_Plans","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Data Management Planning/Data_Management_Plans","description":"As prepared and submitted versions of the Data Management Plans developed by the contributing projects as part of the pilot project. In some cases there are multiple versions available that allow progress to be examined over time. See also the finalised published Data Management Plans for some of the projects that are available through the collection at RIO journal.","hasPart":[{"@id":"data/Data Management Planning/Data_Management_Plans/BVH_BVHDatavs2.docx"},{"@id":"data/Data Management Planning/Data_Management_Plans/DED_DMP-DerechosDigitales-DT.docx"},{"@id":"data/Data Management Planning/Data_Management_Plans/ECV_DMP Vietnam Project (1).docx"},{"@id":"data/Data Management Planning/Data_Management_Plans/HMP_DMP-harassmap-2016-06-02.pdf"},{"@id":"data/Data Management Planning/Data_Management_Plans/IKC_20161107_DMP_IKS & IPR Climate Change Project - Final.pdf"},{"@id":"data/Data Management Planning/Data_Management_Plans/IKC_IKS & IPR Climate Change Project-v1.pdf"},{"@id":"data/Data Management Planning/Data_Management_Plans/NDF_106949_Problemes_negliges_Niger_rdm-plan.doc"},{"@id":"data/Data Management Planning/Data_Management_Plans/NDF_20160504_PGD_Problemes_negliges_FR_Niger (3).doc"},{"@id":"data/Data Management Planning/Data_Management_Plans/TED_TobaccoEconomicData-20160429-project-108098-rdm-plan.pdf"}],"identifier":"./Data Management Planning/Data_Management_Plans","name":"Data Management Plans"},{"@id":"data/Data Management Planning/Data_Management_Plans/BVH_BVHDatavs2.docx","@type":"File","contentSize":"219888","path":"data/Data Management Planning/Data_Management_Plans/BVH_BVHDatavs2.docx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8615-6652"},"creator":{"@id":"http://orcid.org/0000-0001-8615-6652"},"description":"DMP from the BVH project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"BVH_BVHDatavs2.docx","sameAs":"https://doi.org/10.3897/rio.3.e14675"},{"@id":"data/Data Management Planning/Data_Management_Plans/DED_DMP-DerechosDigitales-DT.docx","@type":"File","contentSize":"86127","path":"data/Data Management Planning/Data_Management_Plans/DED_DMP-DerechosDigitales-DT.docx","copyrightHolder":{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219"},"creator":{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219"},"description":"DMP from the DED project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"DED_DMP-DerechosDigitales-DT.docx"},{"@id":"data/Data Management Planning/Data_Management_Plans/ECV_DMP Vietnam Project (1).docx","@type":"File","contentSize":"138056","path":"data/Data Management Planning/Data_Management_Plans/ECV_DMP Vietnam Project (1).docx","copyrightHolder":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"creator":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"description":"DMP from the ECV project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"ECV_DMP Vietnam Project (1).docx"},{"@id":"data/Data Management Planning/Data_Management_Plans/HMP_DMP-harassmap-2016-06-02.pdf","@type":"File","contentSize":"61069","path":"data/Data Management Planning/Data_Management_Plans/HMP_DMP-harassmap-2016-06-02.pdf","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"DMP from the HMP project","encodingFormat":"Acrobat PDF 1.6 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/20","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"HMP_DMP-harassmap-2016-06-02.pdf","sameAs":"https://doi.org/10.3897/rio.3.e15133"},{"@id":"data/Data Management Planning/Data_Management_Plans/IKC_20161107_DMP_IKS & IPR Climate Change Project - Final.pdf","@type":"File","contentSize":"258805","path":"data/Data Management Planning/Data_Management_Plans/IKC_20161107_DMP_IKS & IPR Climate Change Project - Final.pdf","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"DMP from the IKC project v2","encodingFormat":"Acrobat PDF 1.3 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/17","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IKC_20161107_DMP_IKS_IPR_Climate_Change_Project_-_Final.pdf","sameAs":"https://doi.org/10.3897/rio.3.e15111"},{"@id":"data/Data Management Planning/Data_Management_Plans/IKC_IKS & IPR Climate Change Project-v1.pdf","@type":"File","contentSize":"84181","path":"data/Data Management Planning/Data_Management_Plans/IKC_IKS & IPR Climate Change Project-v1.pdf","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"DMP from the IKC project v1","encodingFormat":"Acrobat PDF 1.4 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/18","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IKC_IKS_IPR_Climate_Change_Project-v1.pdf"},{"@id":"data/Data Management Planning/Data_Management_Plans/NDF_106949_Problemes_negliges_Niger_rdm-plan.doc","@type":"File","contentSize":"79360","path":"data/Data Management Planning/Data_Management_Plans/NDF_106949_Problemes_negliges_Niger_rdm-plan.doc","copyrightHolder":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"creator":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"description":"DMP from the NDF project v1","encodingFormat":"Microsoft Word Document","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/40","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"NDF_106949_Problemes_negliges_Niger_rdm-plan.doc"},{"@id":"data/Data Management Planning/Data_Management_Plans/NDF_20160504_PGD_Problemes_negliges_FR_Niger (3).doc","@type":"File","contentSize":"70144","path":"data/Data Management Planning/Data_Management_Plans/NDF_20160504_PGD_Problemes_negliges_FR_Niger (3).doc","copyrightHolder":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"creator":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"description":"DMP from the NDF project v2","encodingFormat":"Microsoft Word Document","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/40","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"NDF_20160504_PGD_Problemes_negliges_FR_Niger_(3).doc"},{"@id":"data/Data Management Planning/Data_Management_Plans/TED_TobaccoEconomicData-20160429-project-108098-rdm-plan.pdf","@type":"File","contentSize":"54165","path":"data/Data Management Planning/Data_Management_Plans/TED_TobaccoEconomicData-20160429-project-108098-rdm-plan.pdf","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"DMP from the TED project","encodingFormat":"Acrobat PDF 1.4 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/18","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"TED_TobaccoEconomicData-20160429-project-108098-rdm-plan.pdf","sameAs":"https://doi.org/10.3897/rio.3.e14837"},{"@id":"data/Data Management Planning/Final_Self_Audit","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Data Management Planning/Final_Self_Audit","description":"In the final project workshop the participating project were invited to conduct a self-audit of their performance against a summary of their Data Management Plan. The templates given to each project are in the directory \"Templates for Self Audit\" This directory contains either an image of the hand completed template or a digital file. For those with images a transcript is also provided. In the case of the NDF project the transcript is a translation of the original French.","hasPart":[{"@id":"data/Data Management Planning/Final_Self_Audit/DED_Constanza-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/DED_IMG_20161202_161808.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/ECV_Trung-Lunch_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/HMP_IMG_20161202_161825.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/HMP_Reem-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_Cath-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161202_161840.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161230_122053.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/NDF_Aissa-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/NDF_IMG_20161230_115536.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/TED_20161202-african-tobacco-data-lw.xlsx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit"}],"identifier":"./Data Management Planning/Final_Self_Audit","name":"Final self audit"},{"@id":"data/Data Management Planning/Final_Self_Audit/DED_Constanza-Lunch_audit_transcript.pdf","@type":"File","contentSize":"25474","path":"data/Data Management Planning/Final_Self_Audit/DED_Constanza-Lunch_audit_transcript.pdf","copyrightHolder":{"@id":"9a4e89e1-13bf-4d44-b5f7-ced40eb33cb2"},"creator":{"@id":"9a4e89e1-13bf-4d44-b5f7-ced40eb33cb2"},"description":"Trancribed version of the DED self audit","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/DED_Constanza-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/DED_IMG_20161202_161808.jpg","@type":"File","contentSize":"1056032","path":"data/Data Management Planning/Final_Self_Audit/DED_IMG_20161202_161808.jpg","copyrightHolder":{"@id":"9a4e89e1-13bf-4d44-b5f7-ced40eb33cb2"},"creator":{"@id":"9a4e89e1-13bf-4d44-b5f7-ced40eb33cb2"},"description":"Image of the DED self audit","encodingFormat":"JPEG File Interchange Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/43","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/DED_IMG_20161202_161808.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/ECV_Trung-Lunch_audit.docx","@type":"File","contentSize":"109827","path":"data/Data Management Planning/Final_Self_Audit/ECV_Trung-Lunch_audit.docx","copyrightHolder":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"creator":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"description":"Digital version of the ECV self audit","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/ECV_Trung-Lunch_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/HMP_IMG_20161202_161825.jpg","@type":"File","contentSize":"1187891","path":"data/Data Management Planning/Final_Self_Audit/HMP_IMG_20161202_161825.jpg","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"Image of the HMP self audit","encodingFormat":"JPEG File Interchange Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/43","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/HMP_IMG_20161202_161825.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/HMP_Reem-Lunch_audit_transcript.pdf","@type":"File","contentSize":"22944","path":"data/Data Management Planning/Final_Self_Audit/HMP_Reem-Lunch_audit_transcript.pdf","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"Trancribed version of the HMP self audit","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/HMP_Reem-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_Cath-Lunch_audit_transcript.pdf","@type":"File","contentSize":"40860","path":"data/Data Management Planning/Final_Self_Audit/IKC_Cath-Lunch_audit_transcript.pdf","copyrightHolder":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"creator":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"description":"Trancribed version of the IKC self audit","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/IKC_Cath-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161202_161840.jpg","@type":"File","contentSize":"1258329","path":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161202_161840.jpg","copyrightHolder":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"creator":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"description":"Image of the IKC self audit part 1","encodingFormat":"JPEG File Interchange Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/43","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/IKC_IMG_20161202_161840.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161230_122053.jpg","@type":"File","contentSize":"2757978","path":"data/Data Management Planning/Final_Self_Audit/IKC_IMG_20161230_122053.jpg","copyrightHolder":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"creator":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"description":"Image of the IKC self audit part 2","encodingFormat":"Exchangeable Image File Format (Compressed)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/x-fmt/391","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/IKC_IMG_20161230_122053.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/NDF_Aissa-Lunch_audit_transcript.pdf","@type":"File","contentSize":"43726","path":"data/Data Management Planning/Final_Self_Audit/NDF_Aissa-Lunch_audit_transcript.pdf","copyrightHolder":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"creator":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"description":"Transcribed version of the NDF self audit","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/NDF_Aissa-Lunch_audit_transcript.pdf"},{"@id":"data/Data Management Planning/Final_Self_Audit/NDF_IMG_20161230_115536.jpg","@type":"File","contentSize":"3835579","path":"data/Data Management Planning/Final_Self_Audit/NDF_IMG_20161230_115536.jpg","copyrightHolder":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"creator":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"description":"Image of the NDF self audit","encodingFormat":"Exchangeable Image File Format (Compressed)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/x-fmt/391","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/NDF_IMG_20161230_115536.jpg"},{"@id":"data/Data Management Planning/Final_Self_Audit/TED_20161202-african-tobacco-data-lw.xlsx","@type":"File","contentSize":"12477","path":"data/Data Management Planning/Final_Self_Audit/TED_20161202-african-tobacco-data-lw.xlsx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Digital version of the TED self audit","encodingFormat":"Microsoft Excel for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/214","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Data Management Planning/Final_Self_Audit/TED_20161202-african-tobacco-data-lw.xlsx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit","description":"The digital version of the documents given to participants in the final workshop to audit their performance against the Data Management Plans that they had previously prepared.","hasPart":[{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_DED_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_ECV_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_HMP_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_IKC_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_NDF_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_TED_self_audit.docx"}],"identifier":"./Data Management Planning/Final_Self_Audit/Templates_for_self_audit","name":"Templates for Self Audit"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_DED_self_audit.docx","@type":"File","contentSize":"70930","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_DED_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for DED project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_DED_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_ECV_self_audit.docx","@type":"File","contentSize":"75046","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_ECV_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for ECV project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_ECV_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_HMP_self_audit.docx","@type":"File","contentSize":"69075","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_HMP_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for HMP project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_HMP_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_IKC_self_audit.docx","@type":"File","contentSize":"75635","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_IKC_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for IKC project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_IKC_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_NDF_self_audit.docx","@type":"File","contentSize":"78214","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_NDF_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for NDF project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_NDF_self_audit.docx"},{"@id":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_TED_self_audit.docx","@type":"File","contentSize":"73224","path":"data/Data Management Planning/Final_Self_Audit/Templates_for_self_audit/AAA_Template_TED_self_audit.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Empty template for data audit for TED project","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Template_TED_self_audit.docx"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments","@type":"Dataset","contactPoint":" Cameron Neylon","path":"data/Data Management Planning/Interim_Report_and_Comments","description":"During the pilot an Interim Report was prepared and used the prompt reponses both from staff within IDRC and from the participating project. This directory contains both the interim report and the responses in varios forms. These include responses as written text documents or appended to the report (BVH, IKC projects and NDF project in French), comments on the original word document of the interim report (IDRC itself, TED).","hasPart":[{"@id":"data/Data Management Planning/Interim_Report_and_Comments/AAA_Data_Audits_and_DMPs_ Interim Report.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/BVH_Virtual_Herbarium_-_Dora_Canhos.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.docx"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IKC_Commentary-DMPs - Cath (IPs and IKS).pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.doc"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAudistandDMPsInterimReport-lw.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAuditsandDMPsInterimReport-lw.docx"}],"identifier":"./Data Management Planning/Interim_Report_and_Comments","name":"Interim Report and Comments"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/AAA_Data_Audits_and_DMPs_ Interim Report.pdf","@type":"File","contentSize":"288599","path":"data/Data Management Planning/Interim_Report_and_Comments/AAA_Data_Audits_and_DMPs_ Interim Report.pdf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The interim report prepared during the project following the DMP interviews but before all the DMPs were completed. Notes progress and initial findings.","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"AAA_Data_Audits_and_DMPs_ Interim Report.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/BVH_Virtual_Herbarium_-_Dora_Canhos.pdf","@type":"File","contentSize":"86161","path":"data/Data Management Planning/Interim_Report_and_Comments/BVH_Virtual_Herbarium_-_Dora_Canhos.pdf","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8615-6652"},"creator":{"@id":"http://orcid.org/0000-0001-8615-6652"},"description":"Response to the interim report from BVH project","encodingFormat":"Acrobat PDF 1.5 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/19","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"BVH_Virtual_Herbarium_-_Dora_Canhos.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.docx","@type":"File","contentSize":"37429","path":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"IDRC"},"description":"Reponse to the interim report from the IDRC team (comments on word doc)","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IDRC_DataAuditsandDMPsInterimReport-comments.docx"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.pdf","@type":"File","contentSize":"70710","path":"data/Data Management Planning/Interim_Report_and_Comments/IDRC_DataAuditsandDMPsInterimReport-comments.pdf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"IDRC"},"description":"Reponse to the interim report from the IDRC team (pdf version)","encodingFormat":"Acrobat PDF 1.3 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/17","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IDRC_DataAuditsandDMPsInterimReport-comments.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/IKC_Commentary-DMPs - Cath (IPs and IKS).pdf","@type":"File","contentSize":"292685","path":"data/Data Management Planning/Interim_Report_and_Comments/IKC_Commentary-DMPs - Cath (IPs and IKS).pdf","copyrightHolder":"Cath Traynor, Laura Foster","creator":[{"@id":"http://orcid.org/0000-0002-9661-487X"},{"@id":"b9947255-54a2-4120-8845-faecf37ae2e8"}],"description":"Response to the interim report from IKC project","encodingFormat":"Acrobat PDF/A - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/95","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"IKC_Commentary-DMPs - Cath (IPs and IKS).pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.doc","@type":"File","contentSize":"64000","path":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.doc","copyrightHolder":"Aïssa Diarra, Pascal Aventurier","creator":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"description":"Response to the interim report from NDF project (addendum to word document)","encodingFormat":"Microsoft Word Document","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/40","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"NDF_WorkOpenDatas_LASDEL_Niger_20161127.doc"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.pdf","@type":"File","contentSize":"111219","path":"data/Data Management Planning/Interim_Report_and_Comments/NDF_WorkOpenDatas_LASDEL_Niger_20161127.pdf","copyrightHolder":"Aïssa Diarra, Pascal Aventurier","creator":[{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},{"@id":"http://orcid.org/0000-0003-0211-4549"}],"description":"Response to the interim report from NDF project (pdf version)","encodingFormat":"Acrobat PDF 1.3 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/17","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"NDF_WorkOpenDatas_LASDEL_Niger_20161127.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAudistandDMPsInterimReport-lw.pdf","@type":"File","contentSize":"374072","path":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAudistandDMPsInterimReport-lw.pdf","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Response to the interim report from TED project (addendum to word document)","encodingFormat":"Acrobat PDF 1.3 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/17","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"TED_20161025-DataAudistandDMPsInterimReport-lw.pdf"},{"@id":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAuditsandDMPsInterimReport-lw.docx","@type":"File","contentSize":"312151","path":"data/Data Management Planning/Interim_Report_and_Comments/TED_20161025-DataAuditsandDMPsInterimReport-lw.docx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Response to the interim report from TED project (pdf version)","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"TED_20161025-DataAuditsandDMPsInterimReport-lw.docx"},{"@id":"data/Final_Project_Workshop_Materials","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Final_Project_Workshop_Materials","description":"The workshop booklets that were used to suppor the second of the two workshops undertaken as part of the workshop. Includes versions with advisor prompts and translations of both forms of the booklet into French.","hasPart":[{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator_francais.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_francais.docx"}],"identifier":"./Final_Project_Workshop_Materials","name":"Final Workshop Materials"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet.docx","@type":"File","contentSize":"655774","path":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The participant booklet and agenda for the final workshop","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"FinalWorkshopBooklet.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator.docx","@type":"File","contentSize":"655719","path":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The facilitator booklet and agenda for the final workshop containing additional prompts and guidance for faciliatators and experts","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"FinalWorkshopBooklet_facilitator.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator_francais.docx","@type":"File","contentSize":"654794","path":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_facilitator_francais.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The participant booklet and agenda for the final workshop translated into French","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"FinalWorkshopBooklet_facilitator_francais.docx"},{"@id":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_francais.docx","@type":"File","contentSize":"657597","path":"data/Final_Project_Workshop_Materials/FinalWorkshopBooklet_francais.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The facilitator booklet and agenda for the final workshop containing additional prompts and guidance for faciliatators and experts, translated into French","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"FinalWorkshopBooklet_francais.docx"},{"@id":"data/Final_Project_Workshop_Presentations","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Final_Project_Workshop_Presentations","description":"The presentations given by the contributing projects at the final project workshop held in Ottawa in December 2016. Each presentation summarised experiences and challenges that the projects had experienced in implementing their Data Management Plans. Includes translations to or from French prepared for most of the participating project presentations.","identifier":"./Final_Project_Workshop_Presentations","name":"Final_Project_Workshop_Presentations","title":"Final Workshop Presentations"},{"@id":"data/Introductory_Data_Workshop_Materials","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Introductory_Data_Workshop_Materials","description":"The first point of contact with the contributing projects was a workshop held at IDRC headquarters in Ottawa from March 7-10 2016. The workshop was supported by a booklet provided to workshop participants which is provided here in several different forms, the participant version, a version with notes for the advisors/mentors and the participant workbook translated into French.","hasPart":[{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletAdvisorNotes.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletFormatted.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipants.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipantsFrancais.docx"}],"identifier":"./Introductory_Data_Workshop_Materials","name":"Introductory Data Workshop Materials"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletAdvisorNotes.docx","@type":"File","contentSize":"1102820","path":"data/Introductory_Data_Workshop_Materials/WorkshopBookletAdvisorNotes.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The booklet for the introductory workshop including the notes for advisors.","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"WorkshopBookletAdvisorNotes.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletFormatted.docx","@type":"File","contentSize":"1102002","path":"data/Introductory_Data_Workshop_Materials/WorkshopBookletFormatted.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The participant booklet from the workshop, including some notes taken during the conduct of the first workshop.","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"WorkshopBookletFormatted.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipants.docx","@type":"File","contentSize":"1091661","path":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipants.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The participant booklet from the workshop.","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"WorkshopBookletParticipants.docx"},{"@id":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipantsFrancais.docx","@type":"File","contentSize":"1093136","path":"data/Introductory_Data_Workshop_Materials/WorkshopBookletParticipantsFrancais.docx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"The participant booklet from the workshop, translated into French.","encodingFormat":"Microsoft Word for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/412","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"WorkshopBookletParticipantsFrancais.docx"},{"@id":"data/Introductory_Workshop_Presentations","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Introductory_Workshop_Presentations","description":"At the introductory workshop each participating project gave a presentation describing their background and project. ","hasPart":[{"@id":"data/Introductory_Workshop_Presentations/BVH_8_OCSDNetDoraIDRC.pptx"},{"@id":"data/Introductory_Workshop_Presentations/DED_4_DanaeTapiaPresentation.pptx"},{"@id":"data/Introductory_Workshop_Presentations/ECV_3_Vietnam-IDRC Workshop.pptx"},{"@id":"data/Introductory_Workshop_Presentations/HMP_1_HarassMap. Ottawa.pptx"},{"@id":"data/Introductory_Workshop_Presentations/IKC_7_IDRC_DataSharing_NaturalJustice.pptx"},{"@id":"data/Introductory_Workshop_Presentations/NDF_6_LASDEL_PNSS_CRDI.ppt"},{"@id":"data/Introductory_Workshop_Presentations/TED_5_20160308-idrc-od-project-woolfrey.pptx"}],"identifier":"./Introductory_Workshop_Presentations","name":"Presentations given at the introductory workshop"},{"@id":"data/Introductory_Workshop_Presentations/BVH_8_OCSDNetDoraIDRC.pptx","@type":"File","contentSize":"3168683","path":"data/Introductory_Workshop_Presentations/BVH_8_OCSDNetDoraIDRC.pptx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8615-6652"},"creator":{"@id":"http://orcid.org/0000-0001-8615-6652"},"description":"Initial presentation to group from BVH project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/BVH_8_OCSDNetDoraIDRC.pptx"},{"@id":"data/Introductory_Workshop_Presentations/DED_4_DanaeTapiaPresentation.pptx","@type":"File","contentSize":"69695","path":"data/Introductory_Workshop_Presentations/DED_4_DanaeTapiaPresentation.pptx","copyrightHolder":{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219"},"creator":{"@id":"d0924b9b-a9ec-4b7f-bc7a-6d2f0582b219"},"description":"Initial presentation to group from DED project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/DED_4_DanaeTapiaPresentation.pptx"},{"@id":"data/Introductory_Workshop_Presentations/ECV_3_Vietnam-IDRC Workshop.pptx","@type":"File","contentSize":"2221793","path":"data/Introductory_Workshop_Presentations/ECV_3_Vietnam-IDRC Workshop.pptx","copyrightHolder":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"creator":{"@id":"a9baaf6c-c315-427e-a9a2-3a9bc06d0605"},"description":"Initial presentation to group from ECV project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/ECV_3_Vietnam-IDRC Workshop.pptx"},{"@id":"data/Introductory_Workshop_Presentations/HMP_1_HarassMap. Ottawa.pptx","@type":"File","contentSize":"515465","path":"data/Introductory_Workshop_Presentations/HMP_1_HarassMap. Ottawa.pptx","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"Initial presentation to group from JMP project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/HMP_1_HarassMap. Ottawa.pptx"},{"@id":"data/Introductory_Workshop_Presentations/IKC_7_IDRC_DataSharing_NaturalJustice.pptx","@type":"File","contentSize":"234088","path":"data/Introductory_Workshop_Presentations/IKC_7_IDRC_DataSharing_NaturalJustice.pptx","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"Initial presentation to group from IKC project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/IKC_7_IDRC_DataSharing_NaturalJustice.pptx"},{"@id":"data/Introductory_Workshop_Presentations/NDF_6_LASDEL_PNSS_CRDI.ppt","@type":"File","contentSize":"121856","path":"data/Introductory_Workshop_Presentations/NDF_6_LASDEL_PNSS_CRDI.ppt","copyrightHolder":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"creator":{"@id":"d1d34769-134f-4747-99eb-4e8deeb140fa"},"description":"Initial presentation to group from NDF project","encodingFormat":"Microsoft Powerpoint Presentation","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/126","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/NDF_6_LASDEL_PNSS_CRDI.ppt"},{"@id":"data/Introductory_Workshop_Presentations/TED_5_20160308-idrc-od-project-woolfrey.pptx","@type":"File","contentSize":"688551","path":"data/Introductory_Workshop_Presentations/TED_5_20160308-idrc-od-project-woolfrey.pptx","copyrightHolder":"Lynn Wollfrey","creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Initial presentation to group from TED project","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Introductory_Workshop_Presentations/TED_5_20160308-idrc-od-project-woolfrey.pptx"},{"@id":"data/Policy and Implementation Review Interviews","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Policy and Implementation Review Interviews","description":"A review of Data Management Planning policy and implementation was comissioned as part of the project (https://doi.org/10.3897/rio.3.e14673). This included interviews with RDM experts on a range of policy and implementation issues. The associated questions were provided to interviewees in advance. Most interviews were conducted at the RDA plenary in Paris, 24-25 September 2015","hasPart":[{"@id":"data/Policy and Implementation Review Interviews/RDAInterviewQuestions.pdf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes"}],"identifier":"./Policy and Implementation Review Interviews","name":"Policy and Implementation Review Interviews","relatedLink":"https://doi.org/10.3897/rio.3.e14673","title":"Data Management Planning Policy and Implementation Reviews"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio","@type":"Dataset","contactPoint":"Cameron Neylon","path":"data/Policy and Implementation Review Interviews/Interview_Audio","description":"For the initial review contributing to the project a series of interviews were conducted with relevant experts on data management and open data policy and implementation. These are the audio records of those interviews.","hasPart":[{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.wav"}],"identifier":"./Policy and Implementation Review Interviews/Interview_Audio","name":"Expert Interviews Audio Files"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.flac","@type":"File","contentSize":"57347219","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Anita de Waard","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-11_11-Anita_de_Waard.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.wav","@type":"File","contentSize":"239478428","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-11_11-Anita_de_Waard.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Anita de Waard","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-11_11-Anita_de_Waard.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.flac","@type":"File","contentSize":"63901469","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Michael Ball","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-13_34-Michael_Ball.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.wav","@type":"File","contentSize":"248399508","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_34-Michael_Ball.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Michael Ball","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-13_34-Michael_Ball.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.flac","@type":"File","contentSize":"35848013","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Carly Strasser","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-13_57-Carly_Strasser.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.wav","@type":"File","contentSize":"147636380","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-13_57-Carly_Strasser.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Carly Strasser","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-13_57-Carly_Strasser.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.flac","@type":"File","contentSize":"69569303","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Patricia Cruze","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-14_28-Patricia_Cruze.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.wav","@type":"File","contentSize":"344629896","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-14_28-Patricia_Cruze.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Patricia Cruze","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-14_28-Patricia_Cruze.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.flac","@type":"File","contentSize":"40903581","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with David Carr","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-15_52-David_Carr.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.wav","@type":"File","contentSize":"224346740","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-15_52-David_Carr.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with David Carr","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-15_52-David_Carr.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.flac","@type":"File","contentSize":"63874559","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Anwar Vahed","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-16_06-Anwar_Vahed.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.wav","@type":"File","contentSize":"327705242","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_06-Anwar_Vahed.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Anwar Vahed","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-16_06-Anwar_Vahed.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].flac","@type":"File","contentSize":"48753386","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Kevin Ashley","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-16_50-Kevin_Ashley[partial].flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].wav","@type":"File","contentSize":"278744572","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-24_09_2015-16_50-Kevin_Ashley[partial].wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Kevin Ashley","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-24_09_2015-16_50-Kevin_Ashley[partial].wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.flac","@type":"File","contentSize":"86485429","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Fiona Murphy","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-09_05-Fiona_Murphy.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.wav","@type":"File","contentSize":"463684252","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-09_05-Fiona_Murphy.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Fiona Murphy","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-09_05-Fiona_Murphy.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.flac","@type":"File","contentSize":"83348503","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Natalia Manola","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-13_09-Natalia_Manola.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.wav","@type":"File","contentSize":"424043172","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_09-Natalia_Manola.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Natalia Manola","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-13_09-Natalia_Manola.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.flac","@type":"File","contentSize":"44904741","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Juan Bicarregui","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-13_43-Juan_Bicarregui.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.wav","@type":"File","contentSize":"243069834","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-13_43-Juan_Bicarregui.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Juan Bicarregui","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-13_43-Juan_Bicarregui.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.flac","@type":"File","contentSize":"79799940","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.flac","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Simon Hodson","encodingFormat":"FLAC (Free Lossless Audio Codec)","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/279","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-14_02-Simon_Hodson.flac"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.wav","@type":"File","contentSize":"425747100","path":"data/Policy and Implementation Review Interviews/Interview_Audio/Interview-25_09_2015-14_02-Simon_Hodson.wav","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Audio of interview with Simon Hodson","encodingFormat":"Waveform Audio","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/6","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Interview-25_09_2015-14_02-Simon_Hodson.wav"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/Policy and Implementation Review Interviews/Interview_Notes","description":"As part of the initial review for the project interviews were conducted with experts on data management planning and sharing policy and implementation. These are the written records and transcripts of those interviews. Most interviews conducted at the RDA Plenary in Paris 25-25 September 2015 ","hasPart":[{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-11_11-Anita_de_Waard.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_34-Michael_Ball.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_57-Carly_Strasser.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-14_28-Patricia_Cruze.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-15_52-David_Carr.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_06-Anwar_Vahed.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_50-Kevin_Ashley[partial].rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-09_05-Fiona_Murphy.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_09-Natalia_Manola.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_43-Juan_Bicarregui.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-14_02-Simon_Hodson.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_11_2015-16_00-Mathew-Harvey.rtf"}],"identifier":"./Policy and Implementation Review Interviews/Interview_Notes","name":"Data Management Expert Interview Notes"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-11_11-Anita_de_Waard.rtf","@type":"File","contentSize":"114629","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-11_11-Anita_de_Waard.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Anita de Waard, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/355","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-11_11-Anita_de_Waard.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_34-Michael_Ball.rtf","@type":"File","contentSize":"384786","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_34-Michael_Ball.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Michael Ball, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_34-Michael_Ball.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_57-Carly_Strasser.rtf","@type":"File","contentSize":"11092","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_57-Carly_Strasser.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Carly Strasser, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/50","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-13_57-Carly_Strasser.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-14_28-Patricia_Cruze.rtf","@type":"File","contentSize":"406854","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-14_28-Patricia_Cruze.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Patricia Cruze, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-14_28-Patricia_Cruze.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-15_52-David_Carr.rtf","@type":"File","contentSize":"340257","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-15_52-David_Carr.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with David Carr, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-15_52-David_Carr.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_06-Anwar_Vahed.rtf","@type":"File","contentSize":"99888","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_06-Anwar_Vahed.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Anwar Vahed, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/355","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_06-Anwar_Vahed.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_50-Kevin_Ashley[partial].rtf","@type":"File","contentSize":"12771","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_50-Kevin_Ashley[partial].rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Kevin Ashley, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/50","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-24_09_2015-16_50-Kevin_Ashley[partial].rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-09_05-Fiona_Murphy.rtf","@type":"File","contentSize":"435251","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-09_05-Fiona_Murphy.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Fiona Murphy, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-09_05-Fiona_Murphy.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_09-Natalia_Manola.rtf","@type":"File","contentSize":"451889","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_09-Natalia_Manola.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Natalia Manola, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_09-Natalia_Manola.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_43-Juan_Bicarregui.rtf","@type":"File","contentSize":"100187","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_43-Juan_Bicarregui.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Juan Bicarregui, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/355","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-13_43-Juan_Bicarregui.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-14_02-Simon_Hodson.rtf","@type":"File","contentSize":"468190","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-14_02-Simon_Hodson.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Simon Hodson, RDA Paris","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-25_09_2015-14_02-Simon_Hodson.rtf"},{"@id":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_11_2015-16_00-Mathew-Harvey.rtf","@type":"File","contentSize":"94235","path":"data/Policy and Implementation Review Interviews/Interview_Notes/Interview-25_11_2015-16_00-Mathew-Harvey.rtf","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Record of interview with Mathew Harvey, by phone. No recording was made of this interview so these notes are the only record.","encodingFormat":"Rich Text Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/53","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/Interview_Notes/Interview-25_11_2015-16_00-Mathew-Harvey.rtf"},{"@id":"data/Policy and Implementation Review Interviews/RDAInterviewQuestions.pdf","@type":"File","contentSize":"51648","path":"data/Policy and Implementation Review Interviews/RDAInterviewQuestions.pdf","creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Interview prompt for interviews with RDM experts","encodingFormat":"Acrobat PDF 1.3 - Portable Document Format","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/17","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Policy and Implementation Review Interviews/RDAInterviewQuestions.pdf"},{"@id":"data/Project Codes.txt","@type":"File","contentSize":"393","path":"data/Project Codes.txt","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Three letter code that links each file to the relevant contributing project of the seven that made the inputs for this Pilot Project","encodingFormat":"Plain Text File","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/x-fmt/111","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"Project Codes.txt"},{"@id":"data/README.txt","@type":"File","contentSize":"3892","path":"data/README.txt","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"README File for the data package. Contains some additional human-readable information and links. The CATALOG.html file is a better source of detailed information on each file and the relations between them.","encodingFormat":"Plain Text File","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/x-fmt/111","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"README.txt"},{"@id":"data/SciDataCon Presentations","@type":"Dataset","contactPoint":{"@id":"http://orcid.org/0000-0002-0068-716X"},"path":"data/SciDataCon Presentations","description":"The Pilot Project submitted a session for the SciDataCon meeting in Denver in 2016 (http://www.scidatacon.org/2016/sessions/56/). A number of the Contributing Projects gave presentations alongside an outline of the initial findings from the Pilot. The separate slide decks and the submitted abstracts from each project are included.","hasPart":[{"@id":"data/SciDataCon Presentations/AAA_20160913-IDRC-SciDataCon Presentation.pptx"},{"@id":"data/SciDataCon Presentations/HMP_HarassMap’s Data Management Project.pptx"},{"@id":"data/SciDataCon Presentations/IKC_Cath Traynor_Should Data be Shared (SciDataCon).pptx"},{"@id":"data/SciDataCon Presentations/TED_20160905-african-tobacco-data-woolfrey.pptx"},{"@id":"data/SciDataCon Presentations/AAA_Pilot_Project_Abstract.html"},{"@id":"data/SciDataCon Presentations/HMP_HarassMap_Abstract.html"},{"@id":"data/SciDataCon Presentations/IKC_Natural_Justice_Abstract.html"},{"@id":"data/SciDataCon Presentations/TED_Tobacco_Economics_Abstract.html"},{"@id":"data/SciDataCon Presentations/AAA_Session_Proposal.html"}],"identifier":"./SciDataCon Presentations","name":"Presentations given at SciDataCon Session","relatedLink":"http://www.scidatacon.org/2016/sessions/56/"},{"@id":"data/SciDataCon Presentations/AAA_20160913-IDRC-SciDataCon Presentation.pptx","@type":"File","contentSize":"14594270","path":"data/SciDataCon Presentations/AAA_20160913-IDRC-SciDataCon Presentation.pptx","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Presentation of the Pilot Project's Initial findings","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/AAA_20160913-IDRC-SciDataCon Presentation.pptx"},{"@id":"data/SciDataCon Presentations/AAA_Pilot_Project_Abstract.html","@type":"File","contentSize":"17085","path":"data/SciDataCon Presentations/AAA_Pilot_Project_Abstract.html","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Abstract for the Pilot Project initial findings","encodingFormat":"Hypertext Markup Language","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/96","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/AAA_Pilot_Project_Abstract.html","sameAs":"http://www.scidatacon.org/2016/sessions/56/paper/265/"},{"@id":"data/SciDataCon Presentations/AAA_Session_Proposal.html","@type":"File","contentSize":"8016","path":"data/SciDataCon Presentations/AAA_Session_Proposal.html","copyrightHolder":{"@id":"IDRC"},"creator":{"@id":"http://orcid.org/0000-0002-0068-716X"},"description":"Session Proposal for SciDataCon","encodingFormat":"Hypertext Markup Language","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/96","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/AAA_Session_Proposal.html","sameAs":"http://www.scidatacon.org/2016/sessions/56/"},{"@id":"data/SciDataCon Presentations/HMP_HarassMap_Abstract.html","@type":"File","contentSize":"5849","path":"data/SciDataCon Presentations/HMP_HarassMap_Abstract.html","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"Abstract for the HarassMap paper","encodingFormat":"Hypertext Markup Language","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/96","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/HMP_HarassMap_Abstract.html","sameAs":"http://www.scidatacon.org/2016/sessions/56/paper/268/"},{"@id":"data/SciDataCon Presentations/HMP_HarassMap’s Data Management Project.pptx","@type":"File","contentSize":"4018305","path":"data/SciDataCon Presentations/HMP_HarassMap’s Data Management Project.pptx","copyrightHolder":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"creator":{"@id":"6e41be89-1635-4d96-b5e1-7d9d88049190"},"description":"Presentation of the DMP project and its experience","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/HMP_HarassMap’s Data Management Project.pptx"},{"@id":"data/SciDataCon Presentations/IKC_Cath Traynor_Should Data be Shared (SciDataCon).pptx","@type":"File","contentSize":"115730","path":"data/SciDataCon Presentations/IKC_Cath Traynor_Should Data be Shared (SciDataCon).pptx","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"Presentation of the IKC project and its experience","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/IKC_Cath Traynor_Should Data be Shared (SciDataCon).pptx"},{"@id":"data/SciDataCon Presentations/IKC_Natural_Justice_Abstract.html","@type":"File","contentSize":"7656","path":"data/SciDataCon Presentations/IKC_Natural_Justice_Abstract.html","copyrightHolder":{"@id":"http://orcid.org/0000-0002-9661-487X"},"creator":{"@id":"http://orcid.org/0000-0002-9661-487X"},"description":"Abstract for the Natural Justice paper","encodingFormat":"Hypertext Markup Language","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/96","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/IKC_Natural_Justice_Abstract.html","sameAs":"http://www.scidatacon.org/2016/sessions/56/paper/271/"},{"@id":"data/SciDataCon Presentations/TED_20160905-african-tobacco-data-woolfrey.pptx","@type":"File","contentSize":"7513480","path":"data/SciDataCon Presentations/TED_20160905-african-tobacco-data-woolfrey.pptx","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Presentation of the TED project and its experience","encodingFormat":"Microsoft Powerpoint for Windows","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/215","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/TED_20160905-african-tobacco-data-woolfrey.pptx"},{"@id":"data/SciDataCon Presentations/TED_Tobacco_Economics_Abstract.html","@type":"File","contentSize":"5880","path":"data/SciDataCon Presentations/TED_Tobacco_Economics_Abstract.html","copyrightHolder":{"@id":"http://orcid.org/0000-0001-8328-7091"},"creator":{"@id":"http://orcid.org/0000-0001-8328-7091"},"description":"Abstract for the Tobacco Economics paper","encodingFormat":"Hypertext Markup Language","fileFormat":"http://www.nationalarchives.gov.uk/PRONOM/fmt/96","license":{"@id":"https://creativecommons.org/licenses/by/4.0/"},"name":"SciDataCon Presentations/TED_Tobacco_Economics_Abstract.html","sameAs":"http://www.scidatacon.org/2016/sessions/56/paper/275/"},{"@id":"DenverSheraton","@type":"Place","URL":"http://www.sheratondenverdowntown.com/?SWAQ=958P","address":"1550 Court Pl, Denver, CO 80202, USA","description":"The location of the 2016 SciDataCon meeting and associated data-related meetings in Denver","identifier":"DenverSheraton","name":"Sheraton Denver Downtown Hotel"},{"@id":"http://orcid.org/0000-0001-5250-7653","@type":"Person","identifier":"http://orcid.org/0000-0001-5250-7653","name":"Juan Bicarregui"},{"@id":"http://orcid.org/0000-0001-7546-5978","@type":"Person","identifier":"http://orcid.org/0000-0001-7546-5978","name":"Kevin Ashley"},{"@id":"http://orcid.org/0000-0001-8328-7091","@type":"Person","identifier":"http://orcid.org/0000-0001-8328-7091","name":"Lynn Woolfrey"},{"@id":"http://orcid.org/0000-0001-8615-6652","@type":"Person","identifier":"http://orcid.org/0000-0001-8615-6652","name":"Dora Canhos"},{"@id":"http://orcid.org/0000-0001-9592-2339","@type":"Person","identifier":"http://orcid.org/0000-0001-9592-2339","name":"Carly Strasser"},{"@id":"http://orcid.org/0000-0002-0068-716X","@type":"Person","contactPoint":{"@id":"cn@cameronneylon.net"},"email":"cn@cameronneylon.net","identifier":"http://orcid.org/0000-0002-0068-716X","name":"Cameron Neylon"},{"@id":"http://orcid.org/0000-0002-3477-3082","@type":"Person","identifier":"http://orcid.org/0000-0002-3477-3082","name":"Natalia Manola"},{"@id":"http://orcid.org/0000-0002-5595-3904","@type":"Person","identifier":"http://orcid.org/0000-0002-5595-3904","name":"Anwar Vahed"},{"@id":"http://orcid.org/0000-0002-9034-4119","@type":"Person","identifier":"http://orcid.org/0000-0002-9034-4119","name":"Anita de Waard"},{"@id":"http://orcid.org/0000-0002-9661-487X","@type":"Person","identifier":"http://orcid.org/0000-0002-9661-487X","name":"Cath Traynor"},{"@id":"http://orcid.org/0000-0003-0211-4549","@type":"Person","identifier":"http://orcid.org/0000-0003-0211-4549","name":"Pascal Aventurier"},{"@id":"http://orcid.org/0000-0003-1435-307X","@type":"Person","identifier":"http://orcid.org/0000-0003-1435-307X","name":"David Carr"},{"@id":"http://orcid.org/0000-0003-1693-1240","@type":"Person","identifier":"http://orcid.org/0000-0003-1693-1240","name":"Fiona Murphy"},{"@id":"http://orcid.org/0000-0003-3179-7270","@type":"Person","identifier":"http://orcid.org/0000-0003-3179-7270","name":"Simon Hodson"},{"@id":"https://creativecommons.org/licenses/by/4.0/","description":"Attribution 4.0 International (CC BY 4.0)\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nYou are free to:\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nShare — copy and redistribute the material in any medium or format\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nAdapt — remix, transform, and build upon the material\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nfor any purpose, even commercially.\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n This license is acceptable for Free Cultural Works.\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nThe licensor cannot revoke these freedoms as long as you follow the license terms.\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nUnder the following terms:\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nAttribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.\r\r\r\r\n\r\r\r\r\n\r\r\r\r\n\r\r\r\r\nNo additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.","identifier":"https://creativecommons.org/licenses/by/4.0/","name":"CC BY 4.0","@type":"Thing"},{"@id":"https://data.research.uts.edu.au/examples/ro-crate/0.2Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies/Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies.zip","contentUrl":"https://data.research.uts.edu.au/examples/ro-crate/0.2Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies/Data_Package-IDRC_Opportunities_and_Challenges_Open_Research_Strategies.zip","@type":"DataDownload","encodingFormat":"zip"},{"@id":"https://doi.org/10.3897/rio.2.e8880","@type":["Project","Organization"],"funder":{"@id":"IDRC"},"identifier":["https://doi.org/10.3897/rio.2.e8880","doi.org/10.3897/rio.2.e8880"],"name":"Exploring the opportunities and challenges of implementing open research strategies within development institutions"},{"@id":"https://www.linkedin.com/in/michael-ball-19236819","@type":"Person","identifier":"https://www.linkedin.com/in/michael-ball-19236819","name":"Michael Ball"},{"@id":"https://www.linkedin.com/in/patricia-trisha-cruse-77a2b39/","@type":"Person","identifier":"https://www.linkedin.com/in/patricia-trisha-cruse-77a2b39/","name":"Patricia Cruze"},{"@id":"IDRC","@type":"Organization","address":"150 Kent Street, Ottawa, Ontario, Canada K1P 0B2","description":"Canadian Frown Corporation and funder of development research","identifier":"IDRC","name":"International Development Research Center"},{"@id":"IDRCHQ","@type":"Place","URL":"https://www.idrc.ca/","address":"150 Kent St, Ottawa, ON K1P 0B2, Canada","description":"The Ottaw Headquarters of the IDRC","identifier":"IDRCHQ","name":"IDRC"},{"@id":"RDA","description":"International organisation for improving research data management practice","identifier":"RDA","name":"Research Data Alliance","@type":"Thing"},{"@type":"CreativeWork","@id":"ro-crate-metadata.json","conformsTo":{"@id":"https://w3id.org/ro/crate/1.1"},"about":{"@id":"./"}}]} \ No newline at end of file diff --git a/core/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json b/core/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json new file mode 100644 index 00000000..c5beefb0 --- /dev/null +++ b/core/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/ro/crate/1.2-DRAFT/context","@graph":[{"@id":"./","identifier":"https://doi.org/10.4225/59/59672c09f4a4b","@type":"Dataset","datePublished":"2017","name":"Data files associated with the manuscript:Effects of facilitated family case conferencing for ...","description":"Palliative care planning for nursing home residents with advanced dementia ...","license":{"@id":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/"}},{"@id":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/","@type":"CreativeWork","description":"This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Australia License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/au/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.","identifier":"https://creativecommons.org/licenses/by-nc-sa/3.0/au/","name":"Attribution-NonCommercial-ShareAlike 3.0 Australia (CC BY-NC-SA 3.0 AU)"},{"@type":"CreativeWork","@id":"ro-crate-metadata.json","about":{"@id":"./"},"conformsTo":[{"@id":"https://w3id.org/ro/crate/1.2-DRAFT"},{"@id":"https://example.com/my/profile/1.0-TEST"}]}]} \ No newline at end of file diff --git a/core/src/test/resources/golden/simple_crate.metadata.json b/core/src/test/resources/golden/simple_crate.metadata.json new file mode 100644 index 00000000..9bb21126 --- /dev/null +++ b/core/src/test/resources/golden/simple_crate.metadata.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/ro/crate/1.1/context","@graph":[{"@id":"./","@type":"Dataset","name":"minimal","description":"minimal RO_crate"},{"@type":"CreativeWork","@id":"ro-crate-metadata.json","conformsTo":{"@id":"https://w3id.org/ro/crate/1.1"},"about":{"@id":"./"}}]} \ No newline at end of file diff --git a/src/test/resources/json/crate/BiggerExample.json b/core/src/test/resources/json/crate/BiggerExample.json similarity index 100% rename from src/test/resources/json/crate/BiggerExample.json rename to core/src/test/resources/json/crate/BiggerExample.json diff --git a/src/test/resources/json/crate/almost-50%.json b/core/src/test/resources/json/crate/almost-50%.json similarity index 100% rename from src/test/resources/json/crate/almost-50%.json rename to core/src/test/resources/json/crate/almost-50%.json diff --git a/src/test/resources/json/crate/fileAndDir.json b/core/src/test/resources/json/crate/fileAndDir.json similarity index 100% rename from src/test/resources/json/crate/fileAndDir.json rename to core/src/test/resources/json/crate/fileAndDir.json diff --git a/src/test/resources/json/crate/onlyOneFile.json b/core/src/test/resources/json/crate/onlyOneFile.json similarity index 100% rename from src/test/resources/json/crate/onlyOneFile.json rename to core/src/test/resources/json/crate/onlyOneFile.json diff --git a/src/test/resources/json/crate/simple.json b/core/src/test/resources/json/crate/simple.json similarity index 100% rename from src/test/resources/json/crate/simple.json rename to core/src/test/resources/json/crate/simple.json diff --git a/src/test/resources/json/crate/simple2.json b/core/src/test/resources/json/crate/simple2.json similarity index 100% rename from src/test/resources/json/crate/simple2.json rename to core/src/test/resources/json/crate/simple2.json diff --git a/src/test/resources/json/crate/twoFiles.json b/core/src/test/resources/json/crate/twoFiles.json similarity index 100% rename from src/test/resources/json/crate/twoFiles.json rename to core/src/test/resources/json/crate/twoFiles.json diff --git a/src/test/resources/json/entities/contextual/createActionExample.json b/core/src/test/resources/json/entities/contextual/createActionExample.json similarity index 100% rename from src/test/resources/json/entities/contextual/createActionExample.json rename to core/src/test/resources/json/entities/contextual/createActionExample.json diff --git a/src/test/resources/json/entities/contextual/createActionExampleSoftware.json b/core/src/test/resources/json/entities/contextual/createActionExampleSoftware.json similarity index 100% rename from src/test/resources/json/entities/contextual/createActionExampleSoftware.json rename to core/src/test/resources/json/entities/contextual/createActionExampleSoftware.json diff --git a/src/test/resources/json/entities/contextual/equipment.json b/core/src/test/resources/json/entities/contextual/equipment.json similarity index 100% rename from src/test/resources/json/entities/contextual/equipment.json rename to core/src/test/resources/json/entities/contextual/equipment.json diff --git a/src/test/resources/json/entities/contextual/orcidperson.json b/core/src/test/resources/json/entities/contextual/orcidperson.json similarity index 100% rename from src/test/resources/json/entities/contextual/orcidperson.json rename to core/src/test/resources/json/entities/contextual/orcidperson.json diff --git a/src/test/resources/json/entities/contextual/organization.json b/core/src/test/resources/json/entities/contextual/organization.json similarity index 100% rename from src/test/resources/json/entities/contextual/organization.json rename to core/src/test/resources/json/entities/contextual/organization.json diff --git a/src/test/resources/json/entities/contextual/person.json b/core/src/test/resources/json/entities/contextual/person.json similarity index 100% rename from src/test/resources/json/entities/contextual/person.json rename to core/src/test/resources/json/entities/contextual/person.json diff --git a/src/test/resources/json/entities/contextual/place.json b/core/src/test/resources/json/entities/contextual/place.json similarity index 100% rename from src/test/resources/json/entities/contextual/place.json rename to core/src/test/resources/json/entities/contextual/place.json diff --git a/src/test/resources/json/entities/contextual/software.json b/core/src/test/resources/json/entities/contextual/software.json similarity index 100% rename from src/test/resources/json/entities/contextual/software.json rename to core/src/test/resources/json/entities/contextual/software.json diff --git a/src/test/resources/json/entities/contextual/updateAction.json b/core/src/test/resources/json/entities/contextual/updateAction.json similarity index 100% rename from src/test/resources/json/entities/contextual/updateAction.json rename to core/src/test/resources/json/entities/contextual/updateAction.json diff --git a/src/test/resources/json/entities/data/directory.json b/core/src/test/resources/json/entities/data/directory.json similarity index 100% rename from src/test/resources/json/entities/data/directory.json rename to core/src/test/resources/json/entities/data/directory.json diff --git a/src/test/resources/json/entities/data/directoryWeb.json b/core/src/test/resources/json/entities/data/directoryWeb.json similarity index 100% rename from src/test/resources/json/entities/data/directoryWeb.json rename to core/src/test/resources/json/entities/data/directoryWeb.json diff --git a/src/test/resources/json/entities/data/fileEntity.json b/core/src/test/resources/json/entities/data/fileEntity.json similarity index 100% rename from src/test/resources/json/entities/data/fileEntity.json rename to core/src/test/resources/json/entities/data/fileEntity.json diff --git a/src/test/resources/json/entities/data/license.json b/core/src/test/resources/json/entities/data/license.json similarity index 100% rename from src/test/resources/json/entities/data/license.json rename to core/src/test/resources/json/entities/data/license.json diff --git a/src/test/resources/json/entities/data/localFile.json b/core/src/test/resources/json/entities/data/localFile.json similarity index 100% rename from src/test/resources/json/entities/data/localFile.json rename to core/src/test/resources/json/entities/data/localFile.json diff --git a/src/test/resources/json/entities/data/localFileWithLicense.json b/core/src/test/resources/json/entities/data/localFileWithLicense.json similarity index 100% rename from src/test/resources/json/entities/data/localFileWithLicense.json rename to core/src/test/resources/json/entities/data/localFileWithLicense.json diff --git a/src/test/resources/json/entities/data/root.json b/core/src/test/resources/json/entities/data/root.json similarity index 100% rename from src/test/resources/json/entities/data/root.json rename to core/src/test/resources/json/entities/data/root.json diff --git a/src/test/resources/json/entities/data/rootMinimalExample.json b/core/src/test/resources/json/entities/data/rootMinimalExample.json similarity index 100% rename from src/test/resources/json/entities/data/rootMinimalExample.json rename to core/src/test/resources/json/entities/data/rootMinimalExample.json diff --git a/src/test/resources/json/entities/data/script1.json b/core/src/test/resources/json/entities/data/script1.json similarity index 100% rename from src/test/resources/json/entities/data/script1.json rename to core/src/test/resources/json/entities/data/script1.json diff --git a/src/test/resources/json/entities/data/workflow.json b/core/src/test/resources/json/entities/data/workflow.json similarity index 100% rename from src/test/resources/json/entities/data/workflow.json rename to core/src/test/resources/json/entities/data/workflow.json diff --git a/src/test/resources/json/unwrap/simple.json b/core/src/test/resources/json/unwrap/simple.json similarity index 100% rename from src/test/resources/json/unwrap/simple.json rename to core/src/test/resources/json/unwrap/simple.json diff --git a/src/test/resources/json/unwrap/simpleResult.json b/core/src/test/resources/json/unwrap/simpleResult.json similarity index 100% rename from src/test/resources/json/unwrap/simpleResult.json rename to core/src/test/resources/json/unwrap/simpleResult.json diff --git a/src/test/resources/spec-v1.1-example-json-files/complete-workflow-example.json b/core/src/test/resources/spec-v1.1-example-json-files/complete-workflow-example.json similarity index 100% rename from src/test/resources/spec-v1.1-example-json-files/complete-workflow-example.json rename to core/src/test/resources/spec-v1.1-example-json-files/complete-workflow-example.json diff --git a/src/test/resources/spec-v1.1-example-json-files/file-author-location.json b/core/src/test/resources/spec-v1.1-example-json-files/file-author-location.json similarity index 100% rename from src/test/resources/spec-v1.1-example-json-files/file-author-location.json rename to core/src/test/resources/spec-v1.1-example-json-files/file-author-location.json diff --git a/src/test/resources/spec-v1.1-example-json-files/files-and-folders.json b/core/src/test/resources/spec-v1.1-example-json-files/files-and-folders.json similarity index 100% rename from src/test/resources/spec-v1.1-example-json-files/files-and-folders.json rename to core/src/test/resources/spec-v1.1-example-json-files/files-and-folders.json diff --git a/src/test/resources/spec-v1.1-example-json-files/minimal.json b/core/src/test/resources/spec-v1.1-example-json-files/minimal.json similarity index 100% rename from src/test/resources/spec-v1.1-example-json-files/minimal.json rename to core/src/test/resources/spec-v1.1-example-json-files/minimal.json diff --git a/src/test/resources/spec-v1.1-example-json-files/web-based-data-entities.json b/core/src/test/resources/spec-v1.1-example-json-files/web-based-data-entities.json similarity index 100% rename from src/test/resources/spec-v1.1-example-json-files/web-based-data-entities.json rename to core/src/test/resources/spec-v1.1-example-json-files/web-based-data-entities.json diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 00000000..7831d264 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,125 @@ +/* + * Executable examples for ro-crate-java that double as a behavior-comparison + * test suite. + * + * How it works: + * + * - src/main the examples written against the current API. They are + * backend-agnostic: they compile against :core only and run + * with whichever JSON backend is on the classpath. + * - src/legacy the same examples written against the released 2.1.2 + * artifact (pre-port Jackson API). Kept in a separate source + * set so the two classpaths never mix. + * + * - Every example is a main() that receives an output file path and writes + * deterministic, canonical content there. + * + * - For each example and variant (see the `variants` map below) a JavaExec + * task runs the example in its own JVM — required, because the JSON + * backends register via ServiceLoader and must not share a classpath — + * and writes build/examples//.txt. + * + * - The parity test (ParityTest) then asserts that consecutive variants + * produce byte-identical output: legacy == current-jackson2 == + * current-jackson3. It derives the variant names from the same `variants` + * map (passed as the rocrate.parity.variants system property) and the + * example list from the output files, so new examples are covered by just + * adding a class and registering it in `exampleClasses` below. + */ + +plugins { + id 'java' +} + +ext { + legacyVersion = '2.1.2' +} + +repositories { + mavenCentral() +} + +sourceSets { + // compiled and run against the released artifact; deliberately not + // sharing a classpath with the current code + legacy +} + +configurations { + // runtime-only backend jars for the current examples; deliberately not on + // the compile classpath so the examples stay backend-agnostic + jackson2Backend + jackson3Backend +} + +dependencies { + implementation project(':core') + // the released artifact exposes Jackson only at runtime scope, so the + // legacy source set declares the same versions for compilation + legacyImplementation "edu.kit.datamanager:ro-crate-java:${legacyVersion}" + legacyImplementation "com.fasterxml.jackson.core:jackson-databind:2.22.1" + jackson2Backend project(':jackson2') + jackson3Backend project(':jackson3') + + testImplementation(platform("org.junit:junit-bom:6.1.3")) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') +} + +/* + * The variants to compare. The key is the variant name and is used as + * - the output directory: build/examples//.txt, + * - the task name fragment: run, and + * - the variant list handed to ParityTest (in map order). + * The classpath closure defines the JVM classpath of the variant; classesTask + * is the task that produces the compiled example classes for it. + */ +def variants = [ + legacy: [ + classesTask: 'legacyClasses', + classpath : { sourceSets.legacy.runtimeClasspath }, + ], + 'current-jackson2': [ + classesTask: 'classes', + classpath : { sourceSets.main.runtimeClasspath + configurations.jackson2Backend }, + ], + 'current-jackson3': [ + classesTask: 'classes', + classpath : { sourceSets.main.runtimeClasspath + configurations.jackson3Backend }, + ], +] + +// register every new example here; the parity test picks it up automatically +def exampleClasses = [ + 'CreateMinimalCrate', + 'ReadCrate', + 'ModifyCrate', + 'WriteCrate', + 'ValidateCrate', +] + +exampleClasses.each { example -> + variants.each { variant, setup -> + tasks.register("run${variant.capitalize()}${example}", JavaExec) { + group = 'examples' + description = "Runs the ${example} example with the ${variant} variant." + dependsOn setup.classesTask + classpath = setup.classpath() + mainClass = "examples.${example}" + def outputFile = layout.buildDirectory.file("examples/${variant}/${example}.txt") + args outputFile.get().asFile.absolutePath + doFirst { + outputFile.get().asFile.parentFile.mkdirs() + } + outputs.file(outputFile) + } + } +} + +tasks.named('test') { + dependsOn exampleClasses.collectMany { example -> + variants.keySet().collect { variant -> tasks.named("run${variant.capitalize()}${example}") } + } + // single source of truth for the variant names used by ParityTest + systemProperty 'rocrate.parity.variants', variants.keySet().join(',') +} diff --git a/examples/src/legacy/java/examples/CanonicalJson.java b/examples/src/legacy/java/examples/CanonicalJson.java new file mode 100644 index 00000000..f195a472 --- /dev/null +++ b/examples/src/legacy/java/examples/CanonicalJson.java @@ -0,0 +1,45 @@ +package examples; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Canonical form of crate metadata, used to compare runs of different library + * versions byte-for-byte: the {@code @graph} entities are sorted by their + * {@code @id} (the entity order follows Java hash iteration and is + * unspecified) and the JSON is written compactly. + * + *

Legacy Jackson implementation for the 2.1.2 examples; the current + * examples ({@code src/main}) use an equivalent neutral-JSON implementation + * of the same class name. + */ +final class CanonicalJson { + + private CanonicalJson() {} + + static String canonicalize(String metadata) throws Exception { + ObjectMapper mapper = MyObjectMapper.getMapper(); + ObjectNode root = (ObjectNode) mapper.readTree(metadata); + JsonNode graph = root.path("@graph"); + if (graph.isArray()) { + List entities = new ArrayList<>(); + graph.forEach(entities::add); + entities.sort( + Comparator.comparing( + entity -> entity.path("@id").asText(""), + String.CASE_INSENSITIVE_ORDER + ) + ); + ArrayNode graphArray = (ArrayNode) graph; + graphArray.removeAll(); + entities.forEach(graphArray::add); + } + return mapper.writeValueAsString(root); + } +} diff --git a/examples/src/legacy/java/examples/CreateMinimalCrate.java b/examples/src/legacy/java/examples/CreateMinimalCrate.java new file mode 100644 index 00000000..682eb8f2 --- /dev/null +++ b/examples/src/legacy/java/examples/CreateMinimalCrate.java @@ -0,0 +1,47 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Legacy variant of {@code CreateMinimalCrate}, written against the released + * 2.1.2 API. The example code is identical to the current variant; only + * {@link CanonicalJson} uses the Jackson API instead of the neutral JSON + * model. + */ +public class CreateMinimalCrate { + + public static void main(String[] args) throws Exception { + RoCrate crate = new RoCrate.RoCrateBuilder( + "Example crate", + "An example crate built with ro-crate-java", + "2024", + "https://creativecommons.org/licenses/by/4.0/" + ) + .addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("survey-responses-2019.csv") + .addType("File") + .addProperty("name", "Survey responses") + .addProperty("contentSize", "26452") + .addProperty("encodingFormat", "text/csv") + .build() + ) + .addContextualEntity( + new ContextualEntity.ContextualEntityBuilder() + .setId("https://orcid.org/0000-0002-1825-0097") + .addType("Person") + .addProperty("name", "Josiah Carberry") + .build() + ) + .build(); + + Files.writeString( + Path.of(args[0]), + CanonicalJson.canonicalize(crate.getJsonMetadata()) + ); + } +} diff --git a/examples/src/legacy/java/examples/ModifyCrate.java b/examples/src/legacy/java/examples/ModifyCrate.java new file mode 100644 index 00000000..1ca59245 --- /dev/null +++ b/examples/src/legacy/java/examples/ModifyCrate.java @@ -0,0 +1,95 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.reader.Readers; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Legacy variant of {@code ModifyCrate}, written against the released 2.1.2 + * API. The example code is identical to the current variant; only + * {@link CanonicalJson} uses the Jackson API instead of the neutral JSON + * model. + */ +public class ModifyCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + RoCrate crate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + StringBuilder report = new StringBuilder(); + + report + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add a data entity, then delete it again + crate.addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("data.txt") + .addType("File") + .addProperty("name", "Some data") + .build() + ); + report + .append("after addDataEntity(data.txt)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("data.txt"); + report + .append("after deleteEntityById(data.txt)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add it back with different properties + crate.addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("data.txt") + .addType("File") + .addProperty("name", "Some other data") + .addProperty("encodingFormat", "text/plain") + .build() + ); + report + .append("after re-adding data.txt with different properties\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // modify properties of the root data entity + crate.getRootDataEntity().removeProperty("datePublished"); + crate.getRootDataEntity().addProperty("keywords", "testing, parity"); + report + .append("after root data entity property changes\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add and remove a contextual entity + ContextualEntity organization = + new ContextualEntity.ContextualEntityBuilder() + .setId("https://ror.org/04jsmm180") + .addType("Organization") + .addProperty("name", "Karlsruhe Institute of Technology") + .build(); + crate.addContextualEntity(organization); + report + .append("after addContextualEntity(organization)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("https://ror.org/04jsmm180"); + report + .append("after deleteEntityById(organization)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())); + + Files.writeString(Path.of(args[0]), report.toString()); + } +} diff --git a/examples/src/legacy/java/examples/ReadCrate.java b/examples/src/legacy/java/examples/ReadCrate.java new file mode 100644 index 00000000..d732fcd4 --- /dev/null +++ b/examples/src/legacy/java/examples/ReadCrate.java @@ -0,0 +1,51 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.writer.Writers; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Legacy variant of {@code ReadCrate}, written against the released 2.1.2 + * API. The example code is identical to the current variant; only + * {@link CanonicalJson} uses the Jackson API instead of the neutral JSON + * model. + */ +public class ReadCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + Path output = Path.of(args[0]); + Path zip = output.getParent().resolve("simple_crate.zip"); + RoCrate folderCrate = Readers.newFolderReader().readCrate( + FIXTURE.toString() + ); + // suppress provenance: it would stamp library version and creation time + // into the metadata and is not part of the comparison + Writers.newZipPathWriter() + .withAutomaticProvenance(null) + .save(folderCrate, zip.toAbsolutePath().toString()); + RoCrate zipCrate = Readers.newZipPathReader().readCrate( + zip.toAbsolutePath().toString() + ); + + StringBuilder report = new StringBuilder(); + report + .append("from folder\n") + .append(CanonicalJson.canonicalize(folderCrate.getJsonMetadata())) + .append("\n\nfrom zip\n") + .append(CanonicalJson.canonicalize(zipCrate.getJsonMetadata())) + .append("\n"); + Files.writeString(output, report.toString()); + } +} diff --git a/examples/src/legacy/java/examples/ValidateCrate.java b/examples/src/legacy/java/examples/ValidateCrate.java new file mode 100644 index 00000000..c38f4c34 --- /dev/null +++ b/examples/src/legacy/java/examples/ValidateCrate.java @@ -0,0 +1,35 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +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 java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Legacy variant of {@code ValidateCrate}, written against the released + * 2.1.2 API. The released version has no {@code Validation} entry point, so + * the JSON-schema validator is instantiated directly; the current code uses + * the backend-agnostic {@code Validation.crateStrategy()} instead. + */ +public class ValidateCrate { + + static final Path CRATES = Path.of("..", "core", "src", "test", "resources", "crates"); + + public static void main(String[] args) throws Exception { + Validator validator = new Validator(new JsonSchemaValidation()); + StringBuilder report = new StringBuilder(); + for (String fixture : List.of("simple_crate", "extendedContextExample", "hasPartHierarchy")) { + RoCrate crate = Readers.newFolderReader() + .readCrate(CRATES.resolve(fixture).toString()); + report + .append(fixture) + .append(": ") + .append(validator.validate(crate)) + .append("\n"); + } + Files.writeString(Path.of(args[0]), report.toString()); + } +} diff --git a/examples/src/legacy/java/examples/WriteCrate.java b/examples/src/legacy/java/examples/WriteCrate.java new file mode 100644 index 00000000..7d078635 --- /dev/null +++ b/examples/src/legacy/java/examples/WriteCrate.java @@ -0,0 +1,119 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.writer.Writers; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Legacy variant of {@code WriteCrate}, written against the released 2.1.2 + * API. The example code is identical to the current variant; only + * {@link CanonicalJson} uses the Jackson API instead of the neutral JSON + * model. + */ +public class WriteCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + Path workDir = Path.of(args[0]).getParent(); + Path folder = workDir.resolve("written-crate"); + Path zip = workDir.resolve("written-crate.zip"); + + RoCrate crate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + Writers.newFolderWriter() + .withAutomaticProvenance(null) + .save(crate, folder.toAbsolutePath().toString()); + Writers.newZipPathWriter() + .withAutomaticProvenance(null) + .save(crate, zip.toAbsolutePath().toString()); + + StringBuilder report = new StringBuilder(); + report + .append("metadata\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\nfolder files (name: sha256)\n") + .append(folderFileDigests(folder)) + .append("\nzip entries (name: sha256)\n") + .append(zipEntryDigests(zip)) + .append("\n"); + Files.writeString(Path.of(args[0]), report.toString()); + } + + static String folderFileDigests(Path folder) throws Exception { + List files; + try (Stream walk = Files.walk(folder)) { + files = walk + .filter(Files::isRegularFile) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + StringBuilder report = new StringBuilder(); + for (Path file : files) { + report + .append(folder.relativize(file).toString()) + .append(": ") + .append(digest(file)) + .append("\n"); + } + return report.toString(); + } + + static String zipEntryDigests(Path zip) throws Exception { + StringBuilder report = new StringBuilder(); + try (ZipFile zipFile = new ZipFile(zip.toFile())) { + List entries = zipFile + .stream() + .sorted(Comparator.comparing(ZipEntry::getName)) + .toList(); + for (ZipEntry entry : entries) { + report + .append(entry.getName()) + .append(": ") + .append(digest(() -> zipFile.getInputStream(entry))) + .append("\n"); + } + } + return report.toString(); + } + + static String digest(Path file) throws Exception { + return digest(() -> Files.newInputStream(file)); + } + + static String digest(InputStreamSupplier supplier) throws Exception { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + try (InputStream stream = supplier.open()) { + byte[] buffer = new byte[8192]; + int read; + while ((read = stream.read(buffer)) != -1) { + sha256.update(buffer, 0, read); + } + } + StringBuilder hex = new StringBuilder(); + for (byte b : sha256.digest()) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + interface InputStreamSupplier { + InputStream open() throws Exception; + } +} diff --git a/examples/src/main/java/examples/CanonicalJson.java b/examples/src/main/java/examples/CanonicalJson.java new file mode 100644 index 00000000..f5bbb77a --- /dev/null +++ b/examples/src/main/java/examples/CanonicalJson.java @@ -0,0 +1,46 @@ +package examples; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Canonical form of crate metadata, used to compare runs of different library + * versions byte-for-byte: the {@code @graph} entities are sorted by their + * {@code @id} (the entity order follows Java hash iteration and is + * unspecified) and the JSON is written compactly. + * + *

This class exists once per source set: this neutral-JSON implementation + * is used by the current examples ({@code src/main}); the legacy 2.1.2 + * examples ({@code src/legacy}) use an equivalent Jackson implementation of + * the same class name. + */ +final class CanonicalJson { + + private CanonicalJson() {} + + static String canonicalize(String metadata) throws Exception { + JsonObject root = (JsonObject) Json.parse(metadata); + JsonValue graph = root.path("@graph"); + if (graph.isArray()) { + List entities = new ArrayList<>(); + graph.forEach(entities::add); + entities.sort( + Comparator.comparing( + entity -> entity.path("@id").asText(""), + String.CASE_INSENSITIVE_ORDER + ) + ); + JsonArray graphArray = (JsonArray) graph; + while (!graphArray.isEmpty()) { + graphArray.remove(0); + } + entities.forEach(graphArray::add); + } + return Json.write(root); + } +} diff --git a/examples/src/main/java/examples/CreateMinimalCrate.java b/examples/src/main/java/examples/CreateMinimalCrate.java new file mode 100644 index 00000000..865010e2 --- /dev/null +++ b/examples/src/main/java/examples/CreateMinimalCrate.java @@ -0,0 +1,49 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Builds a small crate programmatically and writes its metadata in canonical + * form (see {@link CanonicalJson}). + * + *

Run with any backend on the classpath — the example itself is + * backend-agnostic; the ServiceLoader picks up whichever JSON backend + * (jackson2 or jackson3) is present. + */ +public class CreateMinimalCrate { + + public static void main(String[] args) throws Exception { + RoCrate crate = new RoCrate.RoCrateBuilder( + "Example crate", + "An example crate built with ro-crate-java", + "2024", + "https://creativecommons.org/licenses/by/4.0/" + ) + .addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("survey-responses-2019.csv") + .addType("File") + .addProperty("name", "Survey responses") + .addProperty("contentSize", "26452") + .addProperty("encodingFormat", "text/csv") + .build() + ) + .addContextualEntity( + new ContextualEntity.ContextualEntityBuilder() + .setId("https://orcid.org/0000-0002-1825-0097") + .addType("Person") + .addProperty("name", "Josiah Carberry") + .build() + ) + .build(); + + Files.writeString( + Path.of(args[0]), + CanonicalJson.canonicalize(crate.getJsonMetadata()) + ); + } +} diff --git a/examples/src/main/java/examples/ModifyCrate.java b/examples/src/main/java/examples/ModifyCrate.java new file mode 100644 index 00000000..8820ee8f --- /dev/null +++ b/examples/src/main/java/examples/ModifyCrate.java @@ -0,0 +1,95 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.reader.Readers; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Reads a fixture crate from disk and modifies it: adds and removes entities, + * adds and removes properties. Writes the canonical metadata after each step + * (see {@link CanonicalJson}), so the comparison covers the mutation paths of + * the library. + */ +public class ModifyCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + RoCrate crate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + StringBuilder report = new StringBuilder(); + + report + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add a data entity, then delete it again + crate.addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("data.txt") + .addType("File") + .addProperty("name", "Some data") + .build() + ); + report + .append("after addDataEntity(data.txt)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("data.txt"); + report + .append("after deleteEntityById(data.txt)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add it back with different properties + crate.addDataEntity( + new DataEntity.DataEntityBuilder() + .setId("data.txt") + .addType("File") + .addProperty("name", "Some other data") + .addProperty("encodingFormat", "text/plain") + .build() + ); + report + .append("after re-adding data.txt with different properties\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // modify properties of the root data entity + crate.getRootDataEntity().removeProperty("datePublished"); + crate.getRootDataEntity().addProperty("keywords", "testing, parity"); + report + .append("after root data entity property changes\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + + // add and remove a contextual entity + ContextualEntity organization = + new ContextualEntity.ContextualEntityBuilder() + .setId("https://ror.org/04jsmm180") + .addType("Organization") + .addProperty("name", "Karlsruhe Institute of Technology") + .build(); + crate.addContextualEntity(organization); + report + .append("after addContextualEntity(organization)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("https://ror.org/04jsmm180"); + report + .append("after deleteEntityById(organization)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())); + + Files.writeString(Path.of(args[0]), report.toString()); + } +} diff --git a/examples/src/main/java/examples/ReadCrate.java b/examples/src/main/java/examples/ReadCrate.java new file mode 100644 index 00000000..8976409f --- /dev/null +++ b/examples/src/main/java/examples/ReadCrate.java @@ -0,0 +1,50 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.writer.Writers; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Reads a crate from a folder and from a zip archive (the archive is created + * from the fixture with the library's own zip writer) and writes the + * canonical metadata for both paths (see {@link CanonicalJson}). + */ +public class ReadCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + Path output = Path.of(args[0]); + Path zip = output.getParent().resolve("simple_crate.zip"); + RoCrate folderCrate = Readers.newFolderReader().readCrate( + FIXTURE.toString() + ); + // suppress provenance: it would stamp library version and creation time + // into the metadata and is not part of the comparison + Writers.newZipPathWriter() + .withAutomaticProvenance(null) + .save(folderCrate, zip.toAbsolutePath().toString()); + RoCrate zipCrate = Readers.newZipPathReader().readCrate( + zip.toAbsolutePath().toString() + ); + + StringBuilder report = new StringBuilder(); + report + .append("from folder\n") + .append(CanonicalJson.canonicalize(folderCrate.getJsonMetadata())) + .append("\n\nfrom zip\n") + .append(CanonicalJson.canonicalize(zipCrate.getJsonMetadata())) + .append("\n"); + Files.writeString(output, report.toString()); + } +} diff --git a/examples/src/main/java/examples/ValidateCrate.java b/examples/src/main/java/examples/ValidateCrate.java new file mode 100644 index 00000000..390846ba --- /dev/null +++ b/examples/src/main/java/examples/ValidateCrate.java @@ -0,0 +1,48 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.validation.Validation; +import edu.kit.datamanager.ro_crate.validation.Validator; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Validates fixture crates with the default schema and prints the result + * (the boolean only; the human-readable problems go to the error stream and + * are not part of the comparison). The validator is the one provided by the + * JSON backend module on the classpath, obtained through the core's + * {@code Validation} entry point. + */ +public class ValidateCrate { + + static final Path CRATES = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates" + ); + + public static void main(String[] args) throws Exception { + Validator validator = new Validator(Validation.crateStrategy()); + StringBuilder report = new StringBuilder(); + for (String fixture : List.of( + "simple_crate", + "extendedContextExample", + "hasPartHierarchy" + )) { + RoCrate crate = Readers.newFolderReader().readCrate( + CRATES.resolve(fixture).toString() + ); + report + .append(fixture) + .append(": ") + .append(validator.validate(crate)) + .append("\n"); + } + Files.writeString(Path.of(args[0]), report.toString()); + } +} diff --git a/examples/src/main/java/examples/WriteCrate.java b/examples/src/main/java/examples/WriteCrate.java new file mode 100644 index 00000000..36824d45 --- /dev/null +++ b/examples/src/main/java/examples/WriteCrate.java @@ -0,0 +1,120 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.reader.Readers; +import edu.kit.datamanager.ro_crate.writer.Writers; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Reads the fixture crate, writes it to a folder and to a zip archive, and + * reports the written files: the canonical metadata (see + * {@link CanonicalJson}) plus, for every written file and zip entry, its + * name and sha256 hash. Zip entry timestamps and order are deliberately + * excluded (they are not part of the comparison). + */ +public class WriteCrate { + + static final Path FIXTURE = Path.of( + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); + + public static void main(String[] args) throws Exception { + Path workDir = Path.of(args[0]).getParent(); + Path folder = workDir.resolve("written-crate"); + Path zip = workDir.resolve("written-crate.zip"); + + RoCrate crate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + Writers.newFolderWriter() + .withAutomaticProvenance(null) + .save(crate, folder.toAbsolutePath().toString()); + Writers.newZipPathWriter() + .withAutomaticProvenance(null) + .save(crate, zip.toAbsolutePath().toString()); + + StringBuilder report = new StringBuilder(); + report + .append("metadata\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\nfolder files (name: sha256)\n") + .append(folderFileDigests(folder)) + .append("\nzip entries (name: sha256)\n") + .append(zipEntryDigests(zip)) + .append("\n"); + Files.writeString(Path.of(args[0]), report.toString()); + } + + static String folderFileDigests(Path folder) throws Exception { + List files; + try (Stream walk = Files.walk(folder)) { + files = walk + .filter(Files::isRegularFile) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + StringBuilder report = new StringBuilder(); + for (Path file : files) { + report + .append(folder.relativize(file).toString()) + .append(": ") + .append(digest(file)) + .append("\n"); + } + return report.toString(); + } + + static String zipEntryDigests(Path zip) throws Exception { + StringBuilder report = new StringBuilder(); + try (ZipFile zipFile = new ZipFile(zip.toFile())) { + List entries = zipFile + .stream() + .sorted(Comparator.comparing(ZipEntry::getName)) + .toList(); + for (ZipEntry entry : entries) { + report + .append(entry.getName()) + .append(": ") + .append(digest(() -> zipFile.getInputStream(entry))) + .append("\n"); + } + } + return report.toString(); + } + + static String digest(Path file) throws Exception { + return digest(() -> Files.newInputStream(file)); + } + + static String digest(InputStreamSupplier supplier) throws Exception { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + try (InputStream stream = supplier.open()) { + byte[] buffer = new byte[8192]; + int read; + while ((read = stream.read(buffer)) != -1) { + sha256.update(buffer, 0, read); + } + } + StringBuilder hex = new StringBuilder(); + for (byte b : sha256.digest()) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + interface InputStreamSupplier { + InputStream open() throws Exception; + } +} diff --git a/examples/src/test/java/examples/ParityTest.java b/examples/src/test/java/examples/ParityTest.java new file mode 100644 index 00000000..7fbee032 --- /dev/null +++ b/examples/src/test/java/examples/ParityTest.java @@ -0,0 +1,115 @@ +package examples; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +/** + * Compares the outputs of the example runs. Each example is executed once per + * variant (see the {@code variants} map in build.gradle; each run happens in + * its own JVM) and writes {@code build/examples//.txt}. + * + *

This test asserts that consecutive variants produce byte-identical + * output: the released 2.1.2 artifact ({@code legacy}) matches the current + * code with the jackson2 backend, which matches the current code with the + * jackson3 backend. + * + *

New examples are covered automatically: the example list is derived from + * the output files, and the variant names are passed in from the build via + * the {@code rocrate.parity.variants} system property (single source of + * truth: the {@code variants} map in build.gradle). + */ +class ParityTest { + + private static final Path EXAMPLES_DIR = Path.of("build", "examples"); + private static final String VARIANTS_PROPERTY = "rocrate.parity.variants"; + private static final String OUTPUT_SUFFIX = ".txt"; + + private static List variants() { + String value = System.getProperty(VARIANTS_PROPERTY); + assertTrue( + value != null, + "missing system property " + + VARIANTS_PROPERTY + + " — run the tests through the Gradle build" + ); + return List.of(value.split(",")); + } + + /** + * The example names as found in the output of the first variant; also + * asserts that every variant produced the same set of examples. + */ + private static List examples() throws IOException { + List expected = exampleNames(variants().get(0)); + for (String variant : variants()) { + assertEquals( + expected, + exampleNames(variant), + "variant '" + variant + "' produced a different set of examples" + ); + } + return expected; + } + + private static List exampleNames(String variant) throws IOException { + Path dir = EXAMPLES_DIR.resolve(variant); + assertTrue( + Files.isDirectory(dir), + "missing output directory " + + dir + + " — the run tasks should have produced it" + ); + try (Stream files = Files.list(dir)) { + return files + .map(file -> file.getFileName().toString()) + .filter(name -> name.endsWith(OUTPUT_SUFFIX)) + .map(name -> name.substring(0, name.length() - OUTPUT_SUFFIX.length())) + .sorted() + .toList(); + } + } + + private static String output(String variant, String example) + throws IOException { + Path file = EXAMPLES_DIR.resolve(variant).resolve(example + OUTPUT_SUFFIX); + assertTrue( + Files.exists(file), + "missing output " + file + " — the run tasks should have produced it" + ); + return Files.readString(file); + } + + @Test + void everyVariantProducedTheSameExamples() throws IOException { + examples(); + } + + @Test + void consecutiveVariantsProduceIdenticalOutput() throws IOException { + List variants = variants(); + for (String example : examples()) { + for (int i = 0; i < variants.size() - 1; i++) { + String previous = variants.get(i); + String next = variants.get(i + 1); + assertEquals( + output(previous, example), + output(next, example), + "output of '" + + example + + "' differs between '" + + previous + + "' and '" + + next + + "'" + ); + } + } + } +} diff --git a/gradle/module-common.gradle b/gradle/module-common.gradle new file mode 100644 index 00000000..2ec59c21 --- /dev/null +++ b/gradle/module-common.gradle @@ -0,0 +1,44 @@ +/* + * Common configuration applied to every module of ro-crate-java. + */ + +apply plugin: 'java' + +println "Building ${name} version: ${version}" +println "Profile (system property): ${System.getProperty('profile')}" + +java { + //package JavaDoc as part of publication + withJavadocJar() + //package Sources as part of publication + withSourcesJar() + // set java version compatibility + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +if (JavaVersion.current() == JavaVersion.VERSION_17) { + println "Setting encoding to UTF-8 manually" + compileJava.options.encoding = "UTF-8" + compileTestJava.options.encoding = "UTF-8" +} + +// enable -Xlint:deprecation +tasks.withType(JavaCompile).configureEach { + options.compilerArgs << "-Xlint:deprecation" +} + +javadoc { + if (JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + +tasks.withType(Test).configureEach { + // make flags like -Drocrate.golden.update=true visible to the test JVM + systemProperties System.getProperties().findAll { it.key.toString().startsWith('rocrate.') } +} diff --git a/gradle/module-publishing.gradle b/gradle/module-publishing.gradle new file mode 100644 index 00000000..4e33a1bb --- /dev/null +++ b/gradle/module-publishing.gradle @@ -0,0 +1,67 @@ +/* + * Publishing configuration shared by the backend modules. + * + * The publication name is provided by the applying module via the + * 'publishedArtifactId' extra property (defaults to the project name). + */ + +apply plugin: 'maven-publish' +apply plugin: 'signing' + +publishing { + publications { + maven(MavenPublication) { + artifactId = project.findProperty('publishedArtifactId') ?: project.name + from components.java + //set elements in generated pom.xml according to requirements + //by Sonatype (see https://central.sonatype.org/pages/requirements.html) + pom { + name = project['name'] + description = project['description'] + url = 'https://github.com/kit-data-manager/ro-crate-java' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'Pfeil' + name = 'Andreas Pfeil' + email = 'webmaster@datamanager.kit.edu' + } + } + scm { + connection = 'scm:git:github.com/kit-data-manager/ro-crate-java' + developerConnection = 'scm:git:github.com/kit-data-manager/ro-crate-java' + url = 'https://github.com/kit-data-manager/ro-crate-java' + } + } + } + } +} + +signing { + //make signing required unless for SNAPSHOT releases or if signing is explicitly skipped + required { !project.version.endsWith("-SNAPSHOT") && !project.hasProperty("skipSigning") } + + //look for property 'signingKey' + if (project.findProperty("signingKey")) { + //read property 'signingKey' + def signingKey = findProperty("signingKey") + //read property 'signingPassword' + def signingPassword = findProperty("signingPassword") + //use in-memory ascii-armored keys + useInMemoryPgpKeys(signingKey, signingPassword) + //sign publication identity 'publishing.publications.maven' + sign publishing.publications.maven + } else { + println 'WARNING: No property \'signingKey\' found. Artifact signing will be skipped.' + } +} + +def signingTasks = tasks.withType(Sign) +tasks.withType(AbstractPublishToMaven).configureEach { + mustRunAfter(signingTasks) +} diff --git a/gradle/profile-release.gradle b/gradle/profile-release.gradle deleted file mode 100644 index b9a0d003..00000000 --- a/gradle/profile-release.gradle +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2021 Karlsruhe Institute of Technology. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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 { - //define template for tagging, e.g. v1.0.0 - tagTemplate = 'v${version}' -} - -//for java plugin -//see https://docs.gradle.org/current/userguide/java_plugin.html -java { - //package JavaDoc as part of publication - withJavadocJar() - //package Sources as part of publication - withSourcesJar() - // set java version compatibility - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -//for javadoc task -//see https://docs.gradle.org/current/dsl/org.gradle.api.tasks.javadoc.Javadoc.html -javadoc { - if(JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption('html5', true) - } -} - -//for plugin io.github.gradle-nexus.publish-plugin -//see https://github.com/gradle-nexus/publish-plugin -nexusPublishing { - repositories { - //select sonatype repository as publication destination - sonatype { - nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) - snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) - } - } -} - -//for plugin maven-publish/io.github.gradle-nexus.publish-plugin -//see https://docs.gradle.org/current/userguide/publishing_maven.html -publishing { - publications { - //define publication identity, e.g. maven, which will be used - //by the signing plugin, e.g. sign publishing.publications.maven - maven(MavenPublication) { - //select component to publish, e.g. java including JavaDoc - //and sources (see above) - from components.java - //set elements in generated pom.xml according to requirements - //by Sonatype (see https://central.sonatype.org/pages/requirements.html) - pom { - name = project['name'] - description = project['description'] - url = 'https://github.com/kit-data-manager/ro-crate-java' - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'Pfeil' - name = 'Andreas Pfeil' - email = 'webmaster@datamanager.kit.edu' - } - } - scm { - connection = 'scm:git:github.com/kit-data-manager/ro-crate-java' - developerConnection = 'scm:git:github.com/kit-data-manager/ro-crate-java' - url = 'https://github.com/kit-data-manager/ro-crate-java' - } - } - } - } -} - -//for plugin signing -//see https://docs.gradle.org/current/userguide/signing_plugin.html -signing { - //make signing required unless for SNAPSHOT releases or if signing is explicitly skipped - required { !project.version.endsWith("-SNAPSHOT") && !project.hasProperty("skipSigning") } - - //look for property 'signingKey' - if (project.findProperty("signingKey")) { - //If required, read a sub-key specified by its ID in property signingKeyId - //def signingKeyId = findProperty("signingKeyId") - //read property 'signingKey' - def signingKey = findProperty("signingKey") - //read property 'signingPassword' - def signingPassword = findProperty("signingPassword") - //Select to use in-memory ascii-armored keys - useInMemoryPgpKeys(signingKey, signingPassword) - //Only if also using signingKeyId - //useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) - - //Apply signing to publication identity 'publishing.publications.maven' - sign publishing.publications.maven - }else { - println 'WARNING: No property \'signingKey\' found. Artifact signing will be skipped.' - } -} diff --git a/jackson2/build.gradle b/jackson2/build.gradle new file mode 100644 index 00000000..a303cf60 --- /dev/null +++ b/jackson2/build.gradle @@ -0,0 +1,40 @@ +/* + * The Jackson 2 backend of ro-crate-java. + * + * Publishes under the historical artifact coordinates (edu.kit.datamanager: + * ro-crate-java), so consumers of the library keep resolving the same + * artifact; it transitively pulls in the Jackson-free core. + */ + +plugins { + id 'java-library' + id "maven-publish" + id "io.freefair.maven-publish-java" version "9.8.2" +} + +ext { + jacksonVersion = '2.22.4' + // this module publishes under the historical library coordinates + publishedArtifactId = 'ro-crate-java' +} + +description = "A library for easy creation and modification of valid RO-Crates (Jackson 2 backend)." + +repositories { + mavenCentral() +} + +dependencies { + api project(':core') + // JSON object mapping / (de-)serialization + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + // metadata validation, profiles based on JSON schema + implementation group: "com.networknt", name: "json-schema-validator", version: "1.5.9" + + // test parity reference + testImplementation(platform("org.junit:junit-bom:6.1.3")) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') +} + +apply from: rootProject.file('gradle/module-publishing.gradle') diff --git a/jackson2/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java new file mode 100644 index 00000000..51850bf8 --- /dev/null +++ b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java @@ -0,0 +1,101 @@ +package edu.kit.datamanager.ro_crate.entities.validation; + +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson2.Jackson2JsonAdapter; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Objects; +import java.util.Set; + +/** + * Implementation of the entity validation strategy that uses json schema. + * + *

The neutral JSON model is converted to the native tree of the validation + * backend at this boundary; in the split build this class moves to the + * Jackson 2 adapter module. + */ +public class JsonSchemaValidation implements EntityValidationStrategy { + + private static final URL entitySchemaDefault = Objects.requireNonNull( + JsonSchemaValidation.class + .getClassLoader() + .getResource("json_schemas/entity_schema.json") + ); + private static final URL fieldSchemaDefault = Objects.requireNonNull( + JsonSchemaValidation.class + .getClassLoader() + .getResource("json_schemas/entity_field_structure_schema.json") + ); + + private JsonSchema entitySchema; + private JsonSchema entityFieldSchema; + + /** + * Default constructor that uses the default schemas. + */ + public JsonSchemaValidation() { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V201909 + ); + try { + this.entitySchema = factory.getSchema(entitySchemaDefault.toURI()); + this.entityFieldSchema = factory.getSchema(fieldSchemaDefault.toURI()); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + /** + * Constructor that uses custom schemas present as Json files. + * + * @param entitySchema schema for the entities validation. + * @param fieldSchema schema for the field validation. + */ + public JsonSchemaValidation(JsonValue entitySchema, JsonValue fieldSchema) { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V201909 + ); + this.entitySchema = factory.getSchema( + Jackson2JsonAdapter.toJackson(entitySchema) + ); + this.entityFieldSchema = factory.getSchema( + Jackson2JsonAdapter.toJackson(fieldSchema) + ); + } + + @Override + public boolean validateEntity(JsonValue entity) { + Set errors = this.entitySchema.validate( + Jackson2JsonAdapter.toJackson(entity) + ); + if (errors.size() != 0) { + System.err.println( + "This entity does not comply to the basic RO-Crate entity structure." + ); + errors.forEach(error -> System.err.println(error.getMessage())); + return false; + } + return true; + } + + @Override + public boolean validateFieldOfEntity(JsonValue field) { + Set errors = this.entityFieldSchema.validate( + Jackson2JsonAdapter.toJackson(field) + ); + if (!errors.isEmpty()) { + System.err.println("The property: "); + System.err.println(field.toString()); + System.err.println( + "does not comply with the flattened structure" + + " of the RO-Crate json document." + ); + return false; + } + return true; + } +} diff --git a/jackson2/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java new file mode 100644 index 00000000..72d6bf61 --- /dev/null +++ b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java @@ -0,0 +1,204 @@ +package edu.kit.datamanager.ro_crate.json.jackson2; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import edu.kit.datamanager.ro_crate.json.JsonAdapter; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonBoolean; +import edu.kit.datamanager.ro_crate.json.JsonMissing; +import edu.kit.datamanager.ro_crate.json.JsonNull; +import edu.kit.datamanager.ro_crate.json.JsonNumber; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonString; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Jackson 2 implementation of the {@link JsonAdapter} SPI. + * + *

Serialization and parsing are delegated to Jackson 2, so the produced + * JSON is byte-identical to what the library emitted before the conversion. + * The mapper keeps the parse features the library historically enabled on its + * shared mapper (see the former {@code MyObjectMapper}). + */ +public final class Jackson2JsonAdapter implements JsonAdapter { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .enable( + com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY + ) + .enable( + com.fasterxml.jackson.databind.DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS + ); + + @Override + public JsonObject objectNode() { + return JsonObject.create(); + } + + @Override + public JsonArray arrayNode() { + return JsonArray.create(); + } + + @Override + public JsonValue parse(String json) throws IOException { + return toNeutral(MAPPER.readTree(json)); + } + + @Override + public JsonValue parse(File file) throws IOException { + return toNeutral(MAPPER.readTree(file)); + } + + @Override + public JsonValue parse(InputStream stream) throws IOException { + return toNeutral(MAPPER.readTree(stream)); + } + + @Override + public String write(JsonValue value) throws IOException { + return MAPPER.writeValueAsString(toJackson(value)); + } + + @Override + public String writePretty(JsonValue value) throws IOException { + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + toJackson(value) + ); + } + + /** Converts a Jackson 2 tree into the neutral model. */ + public static JsonValue toNeutral(JsonNode node) { + if (node == null || node.isMissingNode()) { + return JsonMissing.INSTANCE; + } + if (node.isObject()) { + JsonObject object = JsonObject.create(); + for (var field : node.properties()) { + object.put(field.getKey(), toNeutral(field.getValue())); + } + return object; + } + if (node.isArray()) { + JsonArray array = JsonArray.create(); + for (JsonNode element : node) { + array.add(toNeutral(element)); + } + return array; + } + if (node.isNull()) { + return JsonNull.INSTANCE; + } + if (node.isTextual()) { + return new JsonString(node.textValue()); + } + if (node.isBoolean()) { + return JsonBoolean.of(node.booleanValue()); + } + if (node.isNumber()) { + return new JsonNumber(numberOf(node)); + } + throw new IllegalArgumentException( + "Unsupported Jackson node type: " + node.getNodeType() + ); + } + + private static Number numberOf(JsonNode node) { + if (node.isInt()) { + return node.intValue(); + } + if (node.isLong()) { + return node.longValue(); + } + if (node.isShort()) { + return node.shortValue(); + } + if (node.isBigInteger()) { + return node.bigIntegerValue(); + } + if (node.isDouble()) { + return node.doubleValue(); + } + if (node.isFloat()) { + return node.floatValue(); + } + if (node.isBigDecimal()) { + return node.decimalValue(); + } + return node.doubleValue(); + } + + /** Converts the neutral model into a Jackson 2 tree. */ + public static JsonNode toJackson(JsonValue value) { + if (value instanceof JsonObject object) { + ObjectNode node = MAPPER.createObjectNode(); + for (var entry : object.entries()) { + node.set(entry.getKey(), toJackson(entry.getValue())); + } + return node; + } + if (value instanceof JsonArray array) { + ArrayNode node = MAPPER.createArrayNode(); + for (JsonValue element : array) { + node.add(toJackson(element)); + } + return node; + } + if (value instanceof JsonString string) { + return MAPPER.getNodeFactory().textNode(string.getValue()); + } + if (value instanceof JsonBoolean bool) { + return bool.getValue() + ? MAPPER.getNodeFactory().booleanNode(true) + : MAPPER.getNodeFactory().booleanNode(false); + } + if (value instanceof JsonNull) { + return MAPPER.getNodeFactory().nullNode(); + } + if (value instanceof JsonMissing) { + return MAPPER.getNodeFactory().missingNode(); + } + if (value instanceof JsonNumber number) { + return numberNode(number.getValue()); + } + throw new IllegalArgumentException( + "Unsupported neutral JSON value: " + value.getClass() + ); + } + + private static JsonNode numberNode(Number number) { + if (number instanceof Integer n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Long n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Short n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Byte n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Double n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Float n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof BigInteger n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof BigDecimal n) { + return MAPPER.getNodeFactory().numberNode(n); + } + // fallback for exotic Number implementations + return MAPPER.getNodeFactory().numberNode(number.doubleValue()); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java similarity index 60% rename from src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java rename to jackson2/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java index 1aed8400..ce64f014 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java +++ b/jackson2/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java @@ -1,15 +1,13 @@ package edu.kit.datamanager.ro_crate.validation; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; - import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson2.Jackson2JsonAdapter; import java.io.File; import java.io.IOException; import java.net.URI; @@ -26,7 +24,9 @@ public class JsonSchemaValidation implements ValidatorStrategy { private JsonSchema schema; private void getSchema(URI schemaUri) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V201909 + ); this.schema = factory.getSchema(schemaUri); } @@ -36,7 +36,8 @@ private void getSchema(URI schemaUri) { public JsonSchemaValidation() { try { URI schemaUri = Objects.requireNonNull( - getClass().getClassLoader().getResource(defaultSchema)).toURI(); + getClass().getClassLoader().getResource(defaultSchema) + ).toURI(); getSchema(schemaUri); } catch (URISyntaxException e) { e.printStackTrace(); @@ -52,23 +53,28 @@ public JsonSchemaValidation(String schema) { getSchema(schemaUri); } - public JsonSchemaValidation(JsonNode schema) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); - this.schema = factory.getSchema(schema); + public JsonSchemaValidation(JsonValue schema) { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V201909 + ); + this.schema = factory.getSchema(Jackson2JsonAdapter.toJackson(schema)); } @Override public boolean validate(Crate crate) { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); try { - final JsonNode good = objectMapper.readTree(crate.getJsonMetadata()); - Set errors = this.schema.validate(good); + final JsonValue good = Json.parse(crate.getJsonMetadata()); + Set errors = this.schema.validate( + Jackson2JsonAdapter.toJackson(good) + ); if (errors.size() == 0) { return true; } else { - System.err.println("This crate does not validate against the this schema." - + " If you haven't provided any schemas," - + " then it does not validate against the default one."); + System.err.println( + "This crate does not validate against the this schema." + + " If you haven't provided any schemas," + + " then it does not validate against the default one." + ); for (var e : errors) { System.err.println(e.getMessage()); } diff --git a/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy new file mode 100644 index 00000000..07c2eba0 --- /dev/null +++ b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.entities.validation.JsonSchemaValidation diff --git a/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter new file mode 100644 index 00000000..e5e0a8aa --- /dev/null +++ b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.json.jackson2.Jackson2JsonAdapter diff --git a/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy new file mode 100644 index 00000000..f9121276 --- /dev/null +++ b/jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation diff --git a/src/main/resources/json_schemas/default.json b/jackson2/src/main/resources/json_schemas/default.json similarity index 100% rename from src/main/resources/json_schemas/default.json rename to jackson2/src/main/resources/json_schemas/default.json diff --git a/src/main/resources/json_schemas/entity_field_structure_schema.json b/jackson2/src/main/resources/json_schemas/entity_field_structure_schema.json similarity index 100% rename from src/main/resources/json_schemas/entity_field_structure_schema.json rename to jackson2/src/main/resources/json_schemas/entity_field_structure_schema.json diff --git a/src/main/resources/json_schemas/entity_schema.json b/jackson2/src/main/resources/json_schemas/entity_schema.json similarity index 100% rename from src/main/resources/json_schemas/entity_schema.json rename to jackson2/src/main/resources/json_schemas/entity_schema.json diff --git a/jackson2/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java b/jackson2/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java new file mode 100644 index 00000000..fe9879d6 --- /dev/null +++ b/jackson2/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java @@ -0,0 +1,83 @@ +package edu.kit.datamanager.ro_crate.json.jackson2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** + * Parity tests: the Jackson 2 adapter must behave like the ObjectMapper the + * library used before the conversion (former {@code MyObjectMapper}), + * byte-for-byte on output and feature-for-feature on parsing. + */ +class Jackson2JsonAdapterParityTest { + + private static final ObjectMapper LEGACY_MAPPER = new ObjectMapper() + .enable( + com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY + ) + .enable( + com.fasterxml.jackson.databind.DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS + ); + + @Test + void parseMatchesLegacyMapperTree() throws IOException { + String json = + "{\"name\":\"crate\",\"items\":[\"one\"],\"nested\":{\"a\":1}}"; + assertEquals( + Jackson2JsonAdapter.toNeutral(LEGACY_MAPPER.readTree(json)), + Json.parse(json) + ); + } + + @Test + void writeMatchesLegacyMapperOutput() throws IOException { + JsonObject object = JsonObject.create() + .put("z", "last") + .put("a", "first") + .put("n", 1) + .put("d", 2.5) + .put("b", true) + .putNull("nil") + .set("items", JsonArray.create().add("x").add("y")); + assertEquals( + LEGACY_MAPPER.writeValueAsString(Jackson2JsonAdapter.toJackson(object)), + Json.write(object) + ); + } + + @Test + void readTreeBehaviorIsIdenticalToLegacyMapper() throws IOException { + // readTree does NOT unwrap single-element arrays even with the legacy + // features enabled (verified against Jackson 2.22.1) — the adapter must + // behave exactly the same + String json = "{\"hasPart\": [{\"@id\": \"a\"}],\"tags\": \"one\"}"; + assertEquals( + Jackson2JsonAdapter.toNeutral(LEGACY_MAPPER.readTree(json)), + Json.parse(json) + ); + assertTrue( + ((JsonObject) Json.parse(json)).get("hasPart").isArray(), + "readTree keeps single-element arrays as arrays" + ); + } + + @Test + void roundTripThroughAdapterIsStable() throws IOException { + String json = + "{\"@context\":\"https://w3id.org/ro/crate/1.1/context\"," + + "\"@graph\":[{\"@id\":\"./\",\"name\":\"m\\\"in\",\"num\":1.0," + + "\"unicode\":\"äöü €\"}]}"; + assertEquals(json, Json.write(Json.parse(json))); + } + + @Test + void adapterIsDiscoverableViaServiceLoader() { + assertEquals(Jackson2JsonAdapter.class, Json.adapter().getClass()); + } +} diff --git a/jackson3/build.gradle b/jackson3/build.gradle new file mode 100644 index 00000000..deee2c63 --- /dev/null +++ b/jackson3/build.gradle @@ -0,0 +1,44 @@ +/* + * The Jackson 3 backend of ro-crate-java (tools.jackson, new package/group). + * + * The Jackson 3 version follows the requirement of the json-schema-validator + * backend (networknt 3.0.x builds against Jackson 3.2.x). + */ + +plugins { + id 'java-library' +} + +ext { + jackson3Version = '3.2.1' + networknt3Version = '3.0.8' + // this module publishes under its own artifact coordinates + publishedArtifactId = 'ro-crate-java-jackson3' +} + +description = "A library for easy creation and modification of valid RO-Crates (Jackson 3 backend)." + +repositories { + mavenCentral() +} + +dependencies { + api project(':core') + // JSON object mapping / (de-)serialization + implementation "tools.jackson.core:jackson-databind:${jackson3Version}" + // metadata validation, profiles based on JSON schema + implementation group: "com.networknt", name: "json-schema-validator", version: networknt3Version + + testImplementation(platform("org.junit:junit-bom:6.1.3")) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') +} + +apply from: rootProject.file('gradle/module-publishing.gradle') + +test { + // the golden files live in the core test resources; make them visible so + // the parity test can compare the Jackson 3 output against the same + // goldens the Jackson 2 backend is verified against + sourceSets.test.resources.srcDir file('../core/src/test/resources') +} diff --git a/jackson3/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/jackson3/JsonSchemaValidation.java b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/jackson3/JsonSchemaValidation.java new file mode 100644 index 00000000..715cdb63 --- /dev/null +++ b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/jackson3/JsonSchemaValidation.java @@ -0,0 +1,95 @@ +package edu.kit.datamanager.ro_crate.entities.validation.jackson3; + +import com.networknt.schema.Error; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SpecificationVersion; +import edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson3.Jackson3JsonAdapter; +import java.util.List; + +/** + * Implementation of the entity validation strategy that uses json schema, + * backed by the Jackson 3 version of the networknt validator (networknt 3.x + * API with the SchemaRegistry). + */ +public class JsonSchemaValidation implements EntityValidationStrategy { + + private static final String entitySchemaDefault = + "classpath:json_schemas/entity_schema.json"; + private static final String fieldSchemaDefault = + "classpath:json_schemas/entity_field_structure_schema.json"; + + private Schema entitySchema; + private Schema entityFieldSchema; + + /** + * Default constructor that uses the default schemas. + */ + public JsonSchemaValidation() { + SchemaRegistry registry = SchemaRegistry.withDefaultDialect( + SpecificationVersion.DRAFT_2019_09 + ); + // the schema files use relative $refs, so they must be loaded with a + // proper base location; the classpath: scheme resolves those correctly + // and works both from folders and jars + this.entitySchema = registry.getSchema( + SchemaLocation.of(entitySchemaDefault) + ); + this.entityFieldSchema = registry.getSchema( + SchemaLocation.of(fieldSchemaDefault) + ); + } + + /** + * Constructor that uses custom schemas present as Json files. + * + * @param entitySchema schema for the entities validation. + * @param fieldSchema schema for the field validation. + */ + public JsonSchemaValidation(JsonValue entitySchema, JsonValue fieldSchema) { + SchemaRegistry registry = SchemaRegistry.withDefaultDialect( + SpecificationVersion.DRAFT_2019_09 + ); + this.entitySchema = registry.getSchema( + Jackson3JsonAdapter.toJackson(entitySchema) + ); + this.entityFieldSchema = registry.getSchema( + Jackson3JsonAdapter.toJackson(fieldSchema) + ); + } + + @Override + public boolean validateEntity(JsonValue entity) { + List errors = this.entitySchema.validate( + Jackson3JsonAdapter.toJackson(entity) + ); + if (!errors.isEmpty()) { + System.err.println( + "This entity does not comply to the basic RO-Crate entity structure." + ); + errors.forEach(error -> System.err.println(error.getMessage())); + return false; + } + return true; + } + + @Override + public boolean validateFieldOfEntity(JsonValue field) { + List errors = this.entityFieldSchema.validate( + Jackson3JsonAdapter.toJackson(field) + ); + if (!errors.isEmpty()) { + System.err.println("The property: "); + System.err.println(field.toString()); + System.err.println( + "does not comply with the flattened structure" + + " of the RO-Crate json document." + ); + return false; + } + return true; + } +} diff --git a/jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java new file mode 100644 index 00000000..4ce22654 --- /dev/null +++ b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java @@ -0,0 +1,209 @@ +package edu.kit.datamanager.ro_crate.json.jackson3; + +import edu.kit.datamanager.ro_crate.json.JsonAdapter; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonBoolean; +import edu.kit.datamanager.ro_crate.json.JsonMissing; +import edu.kit.datamanager.ro_crate.json.JsonNull; +import edu.kit.datamanager.ro_crate.json.JsonNumber; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonString; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +/** + * Jackson 3 implementation of the {@link JsonAdapter} SPI. + * + *

Serialization and parsing are delegated to Jackson 3 + * ({@code tools.jackson}), producing byte-identical output to the Jackson 2 + * backend for the same document. The mapper keeps the parse features the + * library historically enabled on its shared mapper (see the former + * {@code MyObjectMapper}). + * + *

Note on exceptions: Jackson 3 throws unchecked exceptions; the checked + * {@link IOException} in the SPI is kept for interface compatibility. + */ +public final class Jackson3JsonAdapter implements JsonAdapter { + + private static final JsonMapper MAPPER = JsonMapper.builder() + .enable( + tools.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY + ) + .enable( + tools.jackson.databind.DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS + ) + .build(); + + @Override + public JsonObject objectNode() { + return JsonObject.create(); + } + + @Override + public JsonArray arrayNode() { + return JsonArray.create(); + } + + @Override + public JsonValue parse(String json) { + return toNeutral(MAPPER.readTree(json)); + } + + @Override + public JsonValue parse(File file) throws IOException { + return toNeutral(MAPPER.readTree(file)); + } + + @Override + public JsonValue parse(InputStream stream) throws IOException { + return toNeutral(MAPPER.readTree(stream)); + } + + @Override + public String write(JsonValue value) { + return MAPPER.writeValueAsString(toJackson(value)); + } + + @Override + public String writePretty(JsonValue value) { + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + toJackson(value) + ); + } + + /** Converts a Jackson 3 tree into the neutral model. */ + public static JsonValue toNeutral(JsonNode node) { + if (node == null || node.isMissingNode()) { + return JsonMissing.INSTANCE; + } + if (node.isObject()) { + JsonObject object = JsonObject.create(); + for (var field : node.properties()) { + object.put(field.getKey(), toNeutral(field.getValue())); + } + return object; + } + if (node.isArray()) { + JsonArray array = JsonArray.create(); + for (JsonNode element : node) { + array.add(toNeutral(element)); + } + return array; + } + if (node.isNull()) { + return JsonNull.INSTANCE; + } + if (node.isString()) { + return new JsonString(node.stringValue()); + } + if (node.isBoolean()) { + return JsonBoolean.of(node.booleanValue()); + } + if (node.isNumber()) { + return new JsonNumber(numberOf(node)); + } + throw new IllegalArgumentException( + "Unsupported Jackson node type: " + node.getNodeType() + ); + } + + private static Number numberOf(JsonNode node) { + if (node.isInt()) { + return node.intValue(); + } + if (node.isLong()) { + return node.longValue(); + } + if (node.isShort()) { + return node.shortValue(); + } + if (node.isBigInteger()) { + return node.bigIntegerValue(); + } + if (node.isDouble()) { + return node.doubleValue(); + } + if (node.isFloat()) { + return node.floatValue(); + } + if (node.isBigDecimal()) { + return node.decimalValue(); + } + return node.doubleValue(); + } + + /** Converts the neutral model into a Jackson 3 tree. */ + public static JsonNode toJackson(JsonValue value) { + if (value instanceof JsonObject object) { + ObjectNode node = MAPPER.createObjectNode(); + for (var entry : object.entries()) { + node.set(entry.getKey(), toJackson(entry.getValue())); + } + return node; + } + if (value instanceof JsonArray array) { + ArrayNode node = MAPPER.createArrayNode(); + for (JsonValue element : array) { + node.add(toJackson(element)); + } + return node; + } + if (value instanceof JsonString string) { + return MAPPER.getNodeFactory().textNode(string.getValue()); + } + if (value instanceof JsonBoolean bool) { + return bool.getValue() + ? MAPPER.getNodeFactory().booleanNode(true) + : MAPPER.getNodeFactory().booleanNode(false); + } + if (value instanceof JsonNull) { + return MAPPER.getNodeFactory().nullNode(); + } + if (value instanceof JsonMissing) { + return MAPPER.getNodeFactory().missingNode(); + } + if (value instanceof JsonNumber number) { + return numberNode(number.getValue()); + } + throw new IllegalArgumentException( + "Unsupported neutral JSON value: " + value.getClass() + ); + } + + private static JsonNode numberNode(Number number) { + if (number instanceof Integer n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Long n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Short n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Byte n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Double n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof Float n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof BigInteger n) { + return MAPPER.getNodeFactory().numberNode(n); + } + if (number instanceof BigDecimal n) { + return MAPPER.getNodeFactory().numberNode(n); + } + // fallback for exotic Number implementations + return MAPPER.getNodeFactory().numberNode(number.doubleValue()); + } +} diff --git a/jackson3/src/main/java/edu/kit/datamanager/ro_crate/validation/jackson3/JsonSchemaValidation.java b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/validation/jackson3/JsonSchemaValidation.java new file mode 100644 index 00000000..b6c89418 --- /dev/null +++ b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/validation/jackson3/JsonSchemaValidation.java @@ -0,0 +1,89 @@ +package edu.kit.datamanager.ro_crate.validation.jackson3; + +import com.networknt.schema.Error; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SpecificationVersion; +import edu.kit.datamanager.ro_crate.Crate; +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.json.jackson3.Jackson3JsonAdapter; +import edu.kit.datamanager.ro_crate.validation.ValidatorStrategy; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.List; + +/** + * Validation of the crate metadata using JSON-schema, backed by the Jackson 3 + * version of the networknt validator (networknt 3.x API with the + * SchemaRegistry). + */ +public class JsonSchemaValidation implements ValidatorStrategy { + + private static final String defaultSchema = "json_schemas/default.json"; + private Schema schema; + + private void getSchema(URI schemaUri) { + SchemaRegistry registry = SchemaRegistry.withDefaultDialect( + SpecificationVersion.DRAFT_2019_09 + ); + this.schema = registry.getSchema(SchemaLocation.of(schemaUri.toString())); + } + + /** + * Default constructor for the JSON-schema validation. + */ + public JsonSchemaValidation() { + // the classpath: scheme resolves relative $refs correctly and works + // both from folders and jars + SchemaRegistry registry = SchemaRegistry.withDefaultDialect( + SpecificationVersion.DRAFT_2019_09 + ); + this.schema = registry.getSchema( + SchemaLocation.of("classpath:" + defaultSchema) + ); + } + + public JsonSchemaValidation(URI schemaUri) { + getSchema(schemaUri); + } + + public JsonSchemaValidation(String schema) { + URI schemaUri = new File(schema).toURI(); + getSchema(schemaUri); + } + + public JsonSchemaValidation(JsonValue schema) { + SchemaRegistry registry = SchemaRegistry.withDefaultDialect( + SpecificationVersion.DRAFT_2019_09 + ); + this.schema = registry.getSchema(Jackson3JsonAdapter.toJackson(schema)); + } + + @Override + public boolean validate(Crate crate) { + try { + final JsonValue good = Json.parse(crate.getJsonMetadata()); + List errors = this.schema.validate( + Jackson3JsonAdapter.toJackson(good) + ); + if (errors.isEmpty()) { + return true; + } else { + System.err.println( + "This crate does not validate against the this schema." + + " If you haven't provided any schemas," + + " then it does not validate against the default one." + ); + for (var e : errors) { + System.err.println(e.getMessage()); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy new file mode 100644 index 00000000..301835a1 --- /dev/null +++ b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.entities.validation.jackson3.JsonSchemaValidation diff --git a/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter new file mode 100644 index 00000000..ec4080aa --- /dev/null +++ b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.json.jackson3.Jackson3JsonAdapter diff --git a/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy new file mode 100644 index 00000000..7c716296 --- /dev/null +++ b/jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy @@ -0,0 +1 @@ +edu.kit.datamanager.ro_crate.validation.jackson3.JsonSchemaValidation diff --git a/jackson3/src/main/resources/json_schemas/default.json b/jackson3/src/main/resources/json_schemas/default.json new file mode 100644 index 00000000..5d404f94 --- /dev/null +++ b/jackson3/src/main/resources/json_schemas/default.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "required": [ + "@context", + "@graph" + ], + "properties": { + "@graph": { + "type": "array", + "items": { + "type": "object", + "patternProperties": { + ".*": { + "$ref": "entity_field_structure_schema.json" + } + } + } + } + } +} \ No newline at end of file diff --git a/jackson3/src/main/resources/json_schemas/entity_field_structure_schema.json b/jackson3/src/main/resources/json_schemas/entity_field_structure_schema.json new file mode 100644 index 00000000..d2112ebb --- /dev/null +++ b/jackson3/src/main/resources/json_schemas/entity_field_structure_schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Core schema meta-schema", + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/$defs/idProperty"}, + {"type": "string"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "null"} + ] + } + }, + { + "$ref": "#/$defs/idProperty" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "$defs" : { + "idProperty": { + "type": ["object"], + "properties": { + "@id": { + "type": ["string"] + } + }, + "required": ["@id"], + "additionalProperties": false + } + + } +} \ No newline at end of file diff --git a/jackson3/src/main/resources/json_schemas/entity_schema.json b/jackson3/src/main/resources/json_schemas/entity_schema.json new file mode 100644 index 00000000..f8388bd7 --- /dev/null +++ b/jackson3/src/main/resources/json_schemas/entity_schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Schema for the structure of the whole entity", + "type": "object", + "patternProperties": { + ".*": { + "$ref": "entity_field_structure_schema.json" + } + } +} diff --git a/jackson3/src/test/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3BackendParityTest.java b/jackson3/src/test/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3BackendParityTest.java new file mode 100644 index 00000000..84c12c96 --- /dev/null +++ b/jackson3/src/test/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3BackendParityTest.java @@ -0,0 +1,144 @@ +package edu.kit.datamanager.ro_crate.json.jackson3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.kit.datamanager.ro_crate.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import java.io.IOException; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import tools.jackson.databind.json.JsonMapper; + +/** + * Parity tests for the Jackson 3 backend: output must be byte-identical to + * what the Jackson 2 backend produces (and to the committed golden files, + * which were generated with the original Jackson 2 based library). + */ +class Jackson3BackendParityTest { + + private static final JsonMapper REFERENCE_MAPPER = JsonMapper.builder() + .enable( + tools.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY + ) + .enable( + tools.jackson.databind.DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS + ) + .build(); + + @Test + void parseMatchesJackson3Tree() throws IOException { + String json = + "{\"name\":\"crate\",\"items\":[\"one\"],\"nested\":{\"a\":1}}"; + assertEquals( + Jackson3JsonAdapter.toNeutral(REFERENCE_MAPPER.readTree(json)), + Json.parse(json) + ); + } + + @Test + void writeMatchesJackson3Output() throws IOException { + JsonObject object = JsonObject.create() + .put("z", "last") + .put("a", "first") + .put("n", 1) + .put("d", 2.5) + .put("b", true) + .putNull("nil") + .set("items", JsonArray.create().add("x").add("y")); + assertEquals( + REFERENCE_MAPPER.writeValueAsString( + Jackson3JsonAdapter.toJackson(object) + ), + Json.write(object) + ); + } + + @Test + void roundTripThroughAdapterIsStable() throws IOException { + String json = + "{\"@context\":\"https://w3id.org/ro/crate/1.1/context\"," + + "\"@graph\":[{\"@id\":\"./\",\"name\":\"m\\\"in\",\"num\":1.0," + + "\"unicode\":\"äöü €\"}]}"; + assertEquals(json, Json.write(Json.parse(json))); + } + + @Test + void adapterIsDiscoverableViaServiceLoader() { + assertEquals(Jackson3JsonAdapter.class, Json.adapter().getClass()); + } + + /** + * The Jackson 3 backend must produce the same crate metadata as the + * golden files locked in by the core test suite (which were generated + * with the original Jackson 2 based library). + */ + @ParameterizedTest + @MethodSource("fixtureCrates") + void crateMetadataMatchesGoldenFiles(String fixture) throws IOException { + Path crateDir = Path.of("../core/src/test/resources", fixture); + String name = Path.of(fixture).getFileName().toString(); + + edu.kit.datamanager.ro_crate.RoCrate read = + edu.kit.datamanager.ro_crate.reader.Readers.newFolderReader().readCrate( + crateDir.toString() + ); + String metadata = read.getJsonMetadata(); + + Path goldenPath = Path.of( + "../core/src/test/resources/golden", + name + ".metadata.json" + ); + assertTrue( + java.nio.file.Files.exists(goldenPath), + "missing golden file " + goldenPath + ); + String golden = java.nio.file.Files.readString(goldenPath); + assertEquals( + canonicalize(golden), + canonicalize(metadata), + "Jackson 3 backend output drifted from the golden file for '" + name + "'" + ); + } + + /** + * Canonicalizes metadata for byte-level comparison by sorting the + * {@code @graph} entities by their {@code @id} (entity order is unspecified + * in the library). Same semantics as the core golden test's canonicalizer. + */ + private static String canonicalize(String metadata) throws IOException { + JsonObject root = (JsonObject) Json.parse(metadata); + JsonValue graph = root.path("@graph"); + if (graph.isArray()) { + java.util.List entities = new java.util.ArrayList<>(); + graph.forEach(entities::add); + entities.sort( + java.util.Comparator.comparing( + node -> node.path("@id").asText(""), + String.CASE_INSENSITIVE_ORDER + ) + ); + JsonArray graphArray = (JsonArray) graph; + while (!graphArray.isEmpty()) { + graphArray.remove(0); + } + entities.forEach(graphArray::add); + } + return Json.write(root); + } + + static Stream fixtureCrates() { + return Stream.of( + "crates/extendedContextExample", + "crates/hasPartHierarchy", + "crates/simple_crate", + "crates/other/idrc_project", + "crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array" + ); + } +} diff --git a/settings.gradle b/settings.gradle index 2bc86c66..3e70723e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'ro-crate-java' +include 'core', 'jackson2', 'jackson3', 'examples' diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java deleted file mode 100644 index cb960b4a..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ /dev/null @@ -1,693 +0,0 @@ -package edu.kit.datamanager.ro_crate; - -import com.fasterxml.jackson.core.TreeNode; -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.context.CrateMetadataContext; -import edu.kit.datamanager.ro_crate.context.RoCrateMetadataContext; -import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognition; -import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionConfig; -import edu.kit.datamanager.ro_crate.hierarchy.HierarchyRecognitionResult; -import edu.kit.datamanager.ro_crate.entities.AbstractEntity; -import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; -import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; -import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; -import edu.kit.datamanager.ro_crate.externalproviders.dataentities.ImportFromDataCite; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.payload.CratePayload; -import edu.kit.datamanager.ro_crate.payload.RoCratePayload; -import edu.kit.datamanager.ro_crate.preview.CratePreview; -import edu.kit.datamanager.ro_crate.preview.CustomPreview; -import edu.kit.datamanager.ro_crate.special.CrateVersion; -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; -import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.validation.Validator; -import java.io.File; -import java.net.URI; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -/** - * The class that represents a single ROCrate. - *

- * To build or modify it, use a instance of {@link RoCrateBuilder}. In the case - * features of RO-Crate DRAFT specifications are needed, refer to - * {@link BuilderWithDraftFeatures} and its documentation. - * - * @author Nikola Tzotchev on 6.2.2022 г. - * @version 1 - */ -public class RoCrate implements Crate { - - protected final CratePayload roCratePayload; - protected CrateMetadataContext metadataContext; - protected CratePreview roCratePreview; - protected RootDataEntity rootDataEntity; - protected ContextualEntity jsonDescriptor; - - 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; - } - - public void setRoCratePreview(CratePreview preview) { - this.roCratePreview = preview; - } - - @Override - public void setMetadataContext(CrateMetadataContext metadataContext) { - this.metadataContext = metadataContext; - } - - @Override - public String getMetadataContextValueOf(String key) { - return this.metadataContext.getValueOf(key); - } - - @Override - public Set getMetadataContextKeys() { - return this.metadataContext.getKeys(); - } - - @Override - public Map getMetadataContextPairs() { - return this.metadataContext.getPairs(); - } - - public ContextualEntity getJsonDescriptor() { - return jsonDescriptor; - } - - @Override - public void setJsonDescriptor(ContextualEntity jsonDescriptor) { - this.jsonDescriptor = jsonDescriptor; - } - - @Override - public RootDataEntity getRootDataEntity() { - return rootDataEntity; - } - - @Override - public void setRootDataEntity(RootDataEntity rootDataEntity) { - this.rootDataEntity = rootDataEntity; - } - - /** - * Default constructor for creation of an empty crate. - */ - public RoCrate() { - this.roCratePayload = new RoCratePayload(); - this.untrackedFiles = new HashSet<>(); - this.metadataContext = new RoCrateMetadataContext(); - rootDataEntity = new RootDataEntity.RootDataEntityBuilder().build(); - jsonDescriptor = new JsonDescriptor(); - } - - /** - * A constructor for creating the crate using a Crate builder for easier - * creation. - * - * @param roCrateBuilder the builder to use. - */ - public RoCrate(RoCrateBuilder roCrateBuilder) { - this.roCratePayload = roCrateBuilder.payload; - this.metadataContext = roCrateBuilder.metadataContext; - this.roCratePreview = roCrateBuilder.preview; - this.rootDataEntity = roCrateBuilder.rootDataEntity; - this.jsonDescriptor = roCrateBuilder.descriptorBuilder.build(); - this.untrackedFiles = roCrateBuilder.untrackedFiles; - Validator defaultValidation = new Validator(new JsonSchemaValidation()); - defaultValidation.validate(this); - } - - @Override - public Optional getVersion() { - JsonNode conformsTo = this.jsonDescriptor.getProperty("conformsTo"); - if (conformsTo.isArray()) { - return StreamSupport.stream(conformsTo.spliterator(), false) - .filter(TreeNode::isObject) - .map(obj -> obj.path("@id").asText()) - .map(CrateVersion::fromSpecUri) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst(); - } else if (conformsTo.isObject()) { - return CrateVersion.fromSpecUri(conformsTo.get("@id").asText()); - } else { - return Optional.empty(); - } - } - - @Override - public Collection getProfiles() { - JsonNode conformsTo = this.jsonDescriptor.getProperty("conformsTo"); - if (conformsTo.isArray()) { - return StreamSupport.stream(conformsTo.spliterator(), false) - .filter(TreeNode::isObject) - .map(obj -> obj.path("@id").asText()) - .collect(Collectors.toSet()); - } else { - return Collections.emptySet(); - } - } - - @Override - public String getJsonMetadata() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode node = objectMapper.createObjectNode(); - - node.setAll(this.metadataContext.getContextJsonEntity()); - - var graph = objectMapper.createArrayNode(); - ObjectNode root = objectMapper.convertValue( - this.rootDataEntity, - ObjectNode.class - ); - graph.add(root); - - graph.add( - objectMapper.convertValue(this.jsonDescriptor, JsonNode.class) - ); - if ( - this.roCratePayload != null && - this.roCratePayload.getEntitiesMetadata() != null - ) { - graph.addAll(this.roCratePayload.getEntitiesMetadata()); - } - node.set("@graph", graph); - return node.toString(); - } - - @Override - public DataEntity getDataEntityById(String id) { - return this.roCratePayload.getDataEntityById(id); - } - - @Override - public Optional getDataSetById(String id) { - DataEntity data = this.roCratePayload.getDataEntityById(id); - if (data instanceof DataSetEntity) { - return Optional.of((DataSetEntity) data); - } - return Optional.empty(); - } - - @Override - public Set getAllDataEntities() { - return new HashSet<>(this.roCratePayload.getAllDataEntities()); - } - - @Override - public ContextualEntity getContextualEntityById(String id) { - return this.roCratePayload.getContextualEntityById(id); - } - - @Override - public Set getAllContextualEntities() { - return new HashSet<>(this.roCratePayload.getAllContextualEntities()); - } - - @Override - public AbstractEntity getEntityById(String id) { - return this.roCratePayload.getEntityById(id); - } - - /** - * {@inheritDoc} - *

- * Note: This will also link the DataEntity to the root node using the root - * nodes hasPart property. - * - * @param entity the DataEntity to add to this crate. - */ - @Override - public void addDataEntity(DataEntity entity) { - this.metadataContext.checkEntity(entity); - this.roCratePayload.addDataEntity(entity); - this.rootDataEntity.addToHasPart(entity.getId()); - } - - @Override - public void addDataEntity(DataEntity entity, String parentId) - throws IllegalArgumentException { - if (parentId == null) { - throw new IllegalArgumentException("Parent ID is null."); - } - - DataEntity parentEntity = parentId.equals("./") - ? this.getRootDataEntity() - : this.getDataEntityById(parentId); - - if (parentEntity == null) { - throw new IllegalArgumentException( - "Parent ID not found in the crate." - ); - } - - if (parentEntity.getTypes().contains("File")) { - throw new IllegalArgumentException( - "Parent entity cannot be a File." - ); - } - - if (!parentEntity.getTypes().contains("Dataset")) { - throw new IllegalArgumentException( - "Parent entity must be a Dataset in order to contain another DataEntity as a part." - ); - } - - this.metadataContext.checkEntity(entity); - - if (parentEntity instanceof DataSetEntity) { - ((DataSetEntity) parentEntity).addToHasPart(entity.getId()); - } else { - parentEntity.addProperty("hasPart", entity.getId()); - } - this.roCratePayload.addDataEntity(entity); - } - - @Override - public void addContextualEntity(ContextualEntity entity) { - this.metadataContext.checkEntity(entity); - this.roCratePayload.addContextualEntity(entity); - } - - @Override - public void deleteEntityById(String entityId) { - // delete the entity firstly - this.roCratePayload.removeEntityById(entityId); - // remove from the root data entity hasPart - this.rootDataEntity.removeFromHasPart(entityId); - // remove from the root entity and the file descriptor - JsonUtilFunctions.removeFieldsWith( - entityId, - this.rootDataEntity.getProperties() - ); - JsonUtilFunctions.removeFieldsWith( - entityId, - this.jsonDescriptor.getProperties() - ); - } - - @Override - public void setUntrackedFiles(Collection files) { - this.untrackedFiles = files; - } - - @Override - public void deleteValuePairFromContext(String key) { - this.metadataContext.deleteValuePairFromContext(key); - } - - @Override - public void deleteUrlFromContext(String key) { - this.metadataContext.deleteUrlFromContext(key); - } - - @Override - public void addFromCollection( - Collection entities - ) { - this.roCratePayload.addEntities(entities); - } - - @Override - public void addItemFromDataCite(String locationUrl) { - ImportFromDataCite.addDataCiteToCrate(locationUrl, this); - } - - @Override - public Collection getUntrackedFiles() { - return this.untrackedFiles; - } - - @Override - public HierarchyRecognitionResult createDataEntityFileStructure( - boolean addInverseRelationships - ) { - HierarchyRecognitionConfig config = new HierarchyRecognitionConfig() - .withSetInverseRelationships(addInverseRelationships); - return this.createDataEntityFileStructure(config); - } - - @Override - public HierarchyRecognitionResult createDataEntityFileStructure( - HierarchyRecognitionConfig config - ) { - return new HierarchyRecognition(this, config).buildHierarchy(); - } - - /** - * The inner class builder for the easier creation of a ROCrate. - */ - public static class RoCrateBuilder { - - private static final String PROPERTY_DESCRIPTION = "description"; - - CratePayload payload; - CratePreview preview = new CustomPreview(); - CrateMetadataContext metadataContext; - ContextualEntity license; - RootDataEntity rootDataEntity; - Collection untrackedFiles = new HashSet<>(); - - JsonDescriptor.Builder descriptorBuilder = new JsonDescriptor.Builder(); - - /** - * The default constructor of a builder. - * - * @param name the name of the crate. - * @param description the description of the crate. - * @param datePublished the published date of the crate. - * @param licenseId the license identifier of the crate. - */ - public RoCrateBuilder( - String name, - String description, - String datePublished, - String licenseId - ) { - this.payload = new RoCratePayload(); - this.metadataContext = new RoCrateMetadataContext(); - this.rootDataEntity = new RootDataEntity.RootDataEntityBuilder() - .addProperty("name", name) - .addProperty(PROPERTY_DESCRIPTION, description) - .build(); - this.setLicense(licenseId); - this.addDatePublishedWithExceptions(datePublished); - } - - /** - * The default constructor of a builder. - * - * @param name the name of the crate. - * @param description the description of the crate. - * @param datePublished the published date of the crate. - * @param license the license entity of the crate. - */ - public RoCrateBuilder( - String name, - String description, - String datePublished, - ContextualEntity license - ) { - this.payload = new RoCratePayload(); - this.metadataContext = new RoCrateMetadataContext(); - this.rootDataEntity = new RootDataEntity.RootDataEntityBuilder() - .addProperty("name", name) - .addProperty(PROPERTY_DESCRIPTION, description) - .build(); - this.setLicense(license); - this.addDatePublishedWithExceptions(datePublished); - } - - /** - * A default constructor without any params where the root data entity - * will be plain. - */ - public RoCrateBuilder() { - this.payload = new RoCratePayload(); - this.metadataContext = new RoCrateMetadataContext(); - rootDataEntity = new RootDataEntity.RootDataEntityBuilder().build(); - } - - /** - * A constructor with a crate as template. - * - * @param crate the crate to copy. - */ - public RoCrateBuilder(RoCrate crate) { - this.payload = crate.roCratePayload; - this.preview = crate.roCratePreview; - this.metadataContext = crate.metadataContext; - this.rootDataEntity = crate.rootDataEntity; - this.untrackedFiles = crate.untrackedFiles; - this.descriptorBuilder = new JsonDescriptor.Builder(crate); - } - - public RoCrateBuilder addName(String name) { - this.rootDataEntity.addProperty("name", name); - return this; - } - - /** - * Adds an "identifier" property to the root data entity. - *

- * This is useful e.g. to assign e.g. a DOI to this crate. - * @param identifier the identifier to add. - * @return this builder. - */ - public RoCrateBuilder addIdentifier(String identifier) { - this.rootDataEntity.addProperty("identifier", identifier.strip()); - return this; - } - - public RoCrateBuilder addDescription(String description) { - this.rootDataEntity.addProperty(PROPERTY_DESCRIPTION, description); - return this; - } - - /** - * Adds a data entity to the crate. - *

- * Note: This will also link the DataEntity to the root node using the - * root nodes hasPart property. - * - * @param dataEntity the DataEntity to add to this crate. - * @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 void addDataEntity(DataEntity entity, String parentId) - throws IllegalArgumentException { - if (parentId == null) { - throw new IllegalArgumentException("Parent ID is null."); - } - - DataEntity parentEntity = parentId.equals("./") - ? this.rootDataEntity - : this.payload.getDataEntityById(parentId); - - if (parentEntity == null) { - throw new IllegalArgumentException( - "Parent ID not found in the crate." - ); - } - - if (parentEntity.getTypes().contains("File")) { - throw new IllegalArgumentException( - "Parent entity cannot be a File." - ); - } - - if (!parentEntity.getTypes().contains("Dataset")) { - throw new IllegalArgumentException( - "Parent entity must be a Dataset in order to contain another DataEntity as a part." - ); - } - - this.metadataContext.checkEntity(entity); - - if (parentEntity instanceof DataSetEntity) { - ((DataSetEntity) parentEntity).addToHasPart(entity.getId()); - } else { - parentEntity.addProperty("hasPart", entity.getId()); - } - this.payload.addDataEntity(entity); - } - - public RoCrateBuilder addContextualEntity( - ContextualEntity contextualEntity - ) { - this.metadataContext.checkEntity(contextualEntity); - this.payload.addContextualEntity(contextualEntity); - return this; - } - - /** - * Setting the license of the crate. - * - * @param license the license to set. - * @return this builder. - */ - public RoCrateBuilder setLicense(ContextualEntity license) { - this.license = license; - // From our tests, it seems like if we only have the ID for our license, we do - // not need to add an extra entity. - if (license.getProperties().size() > 1) { - this.addContextualEntity(license); - } - this.rootDataEntity.addIdProperty("license", license.getId()); - return this; - } - - /** - * Setting the license of the crate using only a license identifier. - * - * @param licenseId the licenses identifier. Should be a resolveable - * URI. - * @return the builder - */ - public RoCrateBuilder setLicense(String licenseId) { - ContextualEntity licenseEntity = - new ContextualEntity.ContextualEntityBuilder() - .setId(licenseId) - .build(); - this.setLicense(licenseEntity); - return this; - } - - /** - * Adds a property with date time format. The property should match the - * ISO 8601 date format. - * - * @param dateValue time string in ISO 8601 format - * @return this builder - * @throws IllegalArgumentException if format is not ISO 8601 - */ - public RoCrateBuilder addDatePublishedWithExceptions(String dateValue) - throws IllegalArgumentException { - this.rootDataEntity.addDateTimePropertyWithExceptions( - "datePublished", - dateValue - ); - return this; - } - - public RoCrateBuilder setContext(CrateMetadataContext context) { - this.metadataContext = context; - return this; - } - - public RoCrateBuilder addUrlToContext(String url) { - this.metadataContext.addToContextFromUrl(url); - return this; - } - - public RoCrateBuilder addValuePairToContext(String key, String value) { - this.metadataContext.addToContext(key, value); - return this; - } - - public RoCrateBuilder setPreview(CratePreview preview) { - this.preview = preview; - return this; - } - - public RoCrateBuilder addUntrackedFile(File file) { - this.untrackedFiles.add(file); - return this; - } - - /** - * @return a crate with the information from this builder. - */ - public RoCrate build() { - return new RoCrate(this); - } - } - - /** - * Builder for Crates, supporting features which are not in a final - * specification yet. - *

- * NOTE: This will change the specification version of your crate. - *

- * We only add features we expect to be in the new specification in the end. - * In case a feature will not make it into the specification, we will mark - * it as deprecated and remove it in new major versions. If a feature is - * finalized, it will be added to the stable {@link RoCrateBuilder} and - * marked as deprecated in this class. - */ - public static class BuilderWithDraftFeatures extends RoCrateBuilder { - - /** - * @see RoCrateBuilder#RoCrateBuilder() - */ - public BuilderWithDraftFeatures() { - super(); - } - - /** - * @see RoCrateBuilder#RoCrateBuilder(String, String, String, String) - */ - public BuilderWithDraftFeatures( - String name, - String description, - String datePublished, - String licenseId - ) { - super(name, description, datePublished, licenseId); - } - - /** - * @see RoCrateBuilder#RoCrateBuilder(String, String, String, - * ContextualEntity) - */ - public BuilderWithDraftFeatures( - String name, - String description, - String datePublished, - ContextualEntity licenseId - ) { - super(name, description, datePublished, licenseId); - } - - /** - * @see RoCrateBuilder#RoCrateBuilder(RoCrate) - */ - public BuilderWithDraftFeatures(RoCrate crate) { - super(crate); - this.descriptorBuilder = new JsonDescriptor.Builder(crate); - } - - /** - * Indicate this crate also conforms to the given specification, in - * addition to the version this builder adds. - *

- * This is helpful for profiles or other specifications the crate - * conforms to. Can be called multiple times to add more specifications. - * - * @param specification a specification or profile this crate conforms - * to. - * @return the builder - */ - public BuilderWithDraftFeatures alsoConformsTo(URI specification) { - descriptorBuilder - .addConformsTo(specification) - // usage of a draft feature results in draft version numbers of the crate - .setVersion(CrateVersion.LATEST_UNSTABLE); - return this; - } - } -} 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 deleted file mode 100644 index e978b199..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ /dev/null @@ -1,648 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; -import edu.kit.datamanager.ro_crate.entities.serializers.ObjectNodeSerializer; -import edu.kit.datamanager.ro_crate.entities.validation.EntityValidation; -import edu.kit.datamanager.ro_crate.entities.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.payload.Observer; -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; -import edu.kit.datamanager.ro_crate.special.IdentifierUtils; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Abstract Entity parent class of every singe item in the json metadata file. - * - * @author Nikola Tzotchev on 3.2.2022 г. - * @version 1 - */ -public class AbstractEntity { - - /** - * This set contains the types of an entity (ex. File, Dataset, ect.) It is - * a set because it does not make sense to have duplicates - */ - @JsonIgnore - private Set types; - - /** - * Contains the whole list of properties of the entity It uses a custom - * serializer because of cases where a single array element should be - * displayed as a single value. ex: "key" : ["value"] - * <=> "key" : "value" - */ - @JsonUnwrapped - @JsonSerialize(using = ObjectNodeSerializer.class) - private ObjectNode properties; - - private static final EntityValidation entityValidation - = new EntityValidation(new JsonSchemaValidation()); - - /** - * This set contains all the ids of the entities that are linked by - * this entity. This information is provided to crate payloads to make - * the removal of entities faster. - */ - @JsonIgnore - private final Set linkedTo; - - @JsonIgnore - private final Set observers; - - public void addObserver(Observer observer) { - this.observers.add(observer); - } - - private void notifyObservers() { - this.observers.forEach(observer -> observer.update(this.getId())); - } - - /** - * Constructor that takes a builder and instantiates all the fields from it. - * - * @param entityBuilder the entity builder passed to the constructor. - */ - public AbstractEntity(AbstractEntityBuilder entityBuilder) { - this.types = entityBuilder.types; - this.properties = entityBuilder.properties; - this.linkedTo = entityBuilder.relatedItems; - this.observers = new HashSet<>(); - if (this.properties.get("@id") == null) { - if (entityBuilder.id == null) { - this.properties.put("@id", UUID.randomUUID().toString()); - } else { - this.properties.put("@id", entityBuilder.id); - } - } - } - - public Set getLinkedTo() { - return linkedTo; - } - - /** - * Returns the types of this entity. - * - * @return a set of type strings. - */ - public Set getTypes() { - return types; - } - - /** - * Returns a Json object containing the properties of the entity. - * - * @return ObjectNode representing the properties. - */ - public ObjectNode getProperties() { - if (this.types != null) { - JsonNode node = MyObjectMapper.getMapper().valueToTree(this.types); - this.properties.set("@type", node); - } - return properties; - } - - 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) { - return Optional.ofNullable(this.properties.get(propertyKey)) - .map(jsonNode -> jsonNode.path("@id").asText(null)) - .orElse(null); - } - - @JsonIgnore - public String getId() { - JsonNode id = this.properties.get("@id"); - return id == null ? null : id.asText(); - } - - /** - * Set all the properties from a Json object to the Entity. The entities are - * first validated to filter any invalid entity properties. - * - * @param obj the object that contains all the json properties that should - * be added. - */ - public void setProperties(JsonNode obj) { - // validate whole entity - if (entityValidation.entityValidation(obj)) { - this.properties = obj.deepCopy(); - this.notifyObservers(); - } - } - - protected void setId(String id) { - this.properties.put("@id", id); - } - - /** - * removes one property from an entity. - * - * @param key the key of the entity, which will be removed. - */ - public void removeProperty(String key) { - this.getProperties().remove(key); - this.notifyObservers(); - } - - /** - * Removes a collection of properties from an entity. - * - * @param keys collection of keys, which will be removed. - */ - public void removeProperties(Collection keys) { - this.getProperties().remove(keys); - this.notifyObservers(); - } - - /** - * Adds a property to the entity. - *

- * It may override values, if the key already exists. - * - * @param key the key of the property. - * @param value value of the property. - */ - public void addProperty(String key, String value) { - if (key != null && value != null) { - this.properties.put(key, value); - this.notifyObservers(); - } - } - - /** - * Adds a property to the entity. - *

- * It may override values, if the key already exists. - * - * @param key the key of the property. - * @param value value of the property. - */ - public void addProperty(String key, long value) { - if (key != null) { - this.properties.put(key, value); - this.notifyObservers(); - } - } - - /** - * Adds a property to the entity. - *

- * It may override values, if the key already exists. - * - * @param key the key of the property. - * @param value value of the property. - */ - public void addProperty(String key, double value) { - if (key != null) { - this.properties.put(key, value); - this.notifyObservers(); - } - } - - /** - * Adds a generic property to the entity. - *

- * It may fail with an error message on stdout, in which case the - * value is not added. - * It may override values, if the key already exists. - * This is the most generic way to add a property. The value is a - * JsonNode that could contain anything possible. It is limited to - * objects allowed to flattened documents, which means any literal, - * an array of literals, or an object with an @id property. - * - * @param key String key of the property. - * @param value The JsonNode representing the value. - */ - public void addProperty(String key, JsonNode value) { - if (addProperty(this.properties, key, value)) { - notifyObservers(); - } - } - - private static boolean addProperty(ObjectNode whereToAdd, String key, JsonNode value) { - boolean validInput = key != null && value != null; - if (validInput && entityValidation.fieldValidation(value)) { - whereToAdd.set(key, value); - return true; - } - return false; - } - - /** - * Add a property that looks like this: "name" : {"@id" : "id"} If the - * name property already exists add a second @id to it. - * - * @param name the "key" of the property. - * @param id the "id" of the property. - */ - 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)); - this.linkedTo.add(id); - this.notifyObservers(); - } - - /** - * Merges the given id into the current value, - * using this representation: {"@id" : "id"}. - *

- * The current value can be null without errors. - * Only the id will be considered in this case. - *

- * If the id is null-ish, it will not be added, similar to a null-ish value. - * If the id is already present, nothing will be done. - * If it is not an array and the id is not present, an array will be applied. - * - * @param id the id to add. - * @param currentValue the current value of the property. - * @return The updated value of the property. - * Empty if value does not change! - */ - protected static Optional mergeIdIntoValue(String id, JsonNode currentValue) { - if (id == null || id.isBlank()) { return Optional.empty(); } - - ObjectMapper jsonBuilder = MyObjectMapper.getMapper(); - ObjectNode newIdObject = jsonBuilder.createObjectNode().put("@id", id); - if (currentValue == null || currentValue.isNull() || currentValue.isMissingNode()) { - return Optional.ofNullable(newIdObject); - } - - boolean isIdAlready = currentValue.asText().equals(id); - boolean isIdObjectAlready = currentValue.path("@id").asText().equals(id); - boolean isArrayWithIdPresent = currentValue.valueStream() - .anyMatch(node -> node - .path("@id") - .asText() - .equals(id)); - if (isIdAlready || isIdObjectAlready || isArrayWithIdPresent) { - return Optional.empty(); - } - - if (currentValue.isArray() && currentValue instanceof ArrayNode currentValueAsArray) { - currentValueAsArray.add(newIdObject); - return Optional.of(currentValueAsArray); - } else { - // property is not an array, so we make it an array - ArrayNode newNodes = jsonBuilder.createArrayNode(); - newNodes.add(currentValue); - newNodes.add(newIdObject); - return Optional.of(newNodes); - } - } - - /** - * Adds everything from the properties to the property "name" as id. - * - * @param name the key of the property. - * @param properties a collection containing all the id as String. - */ - public void addIdListProperties(String name, Collection properties) { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ArrayNode node = objectMapper.createArrayNode(); - if (this.properties.get(name) == null) { - node = objectMapper.createArrayNode(); - } else { - if (!this.properties.get(name).isArray()) { - node.add(this.properties.get(name)); - } - } - for (String s : properties) { - this.linkedTo.add(s); - node.add(objectMapper.createObjectNode().put("@id", s)); - } - if (node.size() == 1) { - this.properties.set(name, node.get(0)); - } else { - this.properties.set(name, node); - } - notifyObservers(); - } - - /** - * Adding new type to the property (which may have multiple such ones). - * - * @param type the String representing the type. - */ - public void addType(String type) { - if (this.types == null) { - this.types = new HashSet<>(); - } - this.types.add(type); - JsonNode node = MyObjectMapper.getMapper().valueToTree(this.types); - this.properties.set("@type", node); - } - - /** - * Checks if the date matches the ISO 8601 date format. - * - * @param date the date as a string - * @throws IllegalArgumentException if format does not match - */ - private static void checkFormatISO8601(String date) throws IllegalArgumentException { - String regex = "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(date); - if (!matcher.matches()) { - throw new IllegalArgumentException("Date MUST be a string in ISO 8601 format"); - } - } - - /** - * 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) - * @param value time string in ISO 8601 format - * @throws IllegalArgumentException if format is not ISO 8601 - */ - public void addDateTimePropertyWithExceptions(String key, String value) throws IllegalArgumentException { - if (value != null) { - checkFormatISO8601(value); - this.properties.put(key, value); - this.notifyObservers(); - } - } - - /** - * This a builder inner class that should allow for an easier creating of - * entities. - * - * @param The type of the child builders so that they to can use the - * methods provided here. - */ - public abstract static class AbstractEntityBuilder> { - - private Set types; - protected Set relatedItems; - private ObjectNode properties; - private String id; - - protected AbstractEntityBuilder() { - this.properties = MyObjectMapper.getMapper().createObjectNode(); - this.relatedItems = new HashSet<>(); - } - - protected String getId() { - return this.id; - } - - /** - * Setting the id property of the entity, if the given value is not - * null. If the id is not encoded, the encoding will be done. - *

- * NOTE: IDs are not just names! The ID may have effects - * on parts of your crate! For example: If the entity represents a - * file which will be copied into the crate, writers must use the - * ID as filename. - * - * @param id the String representing the id. - * @return the generic builder. - */ - public T setId(String id) { - if (id != null && !id.equals(RootDataEntity.ID)) { - if (IdentifierUtils.isValidUri(id)) { - this.id = id; - } else { - this.id = IdentifierUtils.encode(id).orElse(this.id); - } - } - return self(); - } - - /** - * Adding a type to the builder types. - * - * @param type the type to add. - * @return the generic builder. - */ - public T addType(String type) { - if (this.types == null) { - this.types = new HashSet<>(); - } - if (type != null && !type.isEmpty()) { - this.types.add(type); - } - return self(); - } - - /** - * Adds multiple types in one call. - * - * @param types the types to add. - * @return the generic builder. - */ - public T addTypes(Collection types) { - if (this.types == null) { - this.types = new HashSet<>(); - } - this.types.addAll(types); - return self(); - } - - /** - * 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) - * @param value time string in ISO 8601 format - * @return this builder - * @throws IllegalArgumentException if format is not ISO 8601 - */ - public T addDateTimePropertyWithExceptions(String key, String value) throws IllegalArgumentException { - if (value != null) { - checkFormatISO8601(value); - this.properties.put(key, value); - } - return self(); - } - - /** - * Adding a property to the builder. - * - * @param key the key of the property in a string. - * @param value the JsonNode value of te property. - * @return the generic builder. - */ - public T addProperty(String key, JsonNode value) { - if (AbstractEntity.addProperty(this.properties, key, value)) { - this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromProperty(value)); - } - return self(); - } - - /** - * Adding a property to the builder. - * - * @param key the key of the property as a string. - * @param value the value of the property as a string. - * @return the generic builder. - */ - public T addProperty(String key, String value) { - this.properties.put(key, value); - return self(); - } - - public T addProperty(String key, int value) { - this.properties.put(key, value); - return self(); - } - - public T addProperty(String key, double value) { - this.properties.put(key, value); - return self(); - } - - public T addProperty(String key, boolean value) { - this.properties.put(key, value); - return self(); - } - - /** - * 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"} } - * - * Does nothing if name or id are null. - * - * @param name the name of the ID property. - * @param id the ID. - * @return the generic builder - */ - public T addIdProperty(String name, String id) { - AbstractEntity.mergeIdIntoValue(id, this.properties.get(name)) - .ifPresent(newValue -> { - this.properties.set(name, newValue); - this.relatedItems.add(id); - }); - return self(); - } - - /** - * This is another way of adding the ID property, this time the whole - * other Entity is provided. - * - * @param name the name of the property. - * @param entity the other entity that is referenced. - * @return the generic builder. - */ - public T addIdProperty(String name, AbstractEntity entity) { - if (entity != null) { - return addIdProperty(name, entity.getId()); - } - return self(); - } - - /** - * This adds multiple id entities to a single key. - * - * @param name the name of the property. - * @param entities the Collection containing the multiple entities. - * @return the generic builder. - */ - public T addIdFromCollectionOfEntities(String name, Collection entities) { - if (entities != null) { - for (var e : entities) { - addIdProperty(name, e); - } - } - return self(); - } - - /** - * Deprecated. Equivalent to {@link #setAllIfValid(ObjectNode)}. - * - * @param properties the Json representing all the properties. - * @return the generic builder, either including all given properties - * * or unchanged. - * - * @deprecated To enforce the user know what this method does, - * we want the user to use one of the more explicitly named - * methods {@link #setAllIfValid(ObjectNode)} or - * {@link #setAllIfValid(ObjectNode)}. - * @see #setAllIfValid(ObjectNode) - */ - @Deprecated(since = "2.1.0", forRemoval = true) - public T setAll(ObjectNode properties) { - return setAllIfValid(properties); - } - - /** - * This sets everything from a json object to the property, - * if the result is valid. Otherwise, it will do nothing. - *

- * Valid means here that the json object needs to be flat as specified - * in the RO-Crate specification. In principle, this means that - * primitives and objects referencing an ID are allowed, - * as well as arrays of these. - * - * @param properties the Json representing all the properties. - * @return the generic builder, either including all given properties - * or unchanged. - */ - public T setAllIfValid(ObjectNode properties) { - if (AbstractEntity.entityValidation.entityValidation(properties)) { - this.properties = properties; - this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromJsonNode(properties)); - } - return self(); - } - - /** - * This sets everything from a json object to the property. Can be - * useful when the entity is already available somewhere. - *

- * Errors on validation are printed, but everything will be added. - * For more about validation, see {@link #setAllIfValid(ObjectNode)}. - * - * @param properties the Json representing all the properties. - * @return the generic builder with all properties added. - */ - public T setAllUnsafe(ObjectNode properties) { - // This will currently only print errors. - AbstractEntity.entityValidation.entityValidation(properties); - this.properties = properties; - JsonNode typeProps = properties.path("@type"); - if (typeProps.isArray()) { - typeProps.valueStream() - .forEach(value -> this.addType(value.asText())); - } else if (typeProps.isTextual()) { - this.addType(typeProps.asText()); - } - this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromJsonNode(properties)); - return self(); - } - - public abstract T self(); - - public abstract AbstractEntity build(); - } - -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java deleted file mode 100644 index 8136052a..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java +++ /dev/null @@ -1,191 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.contextual; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Optional; - -/** - * This class represents a detailing provenance of entities. - * - * Relevant specification link: Provenance - * of entities - * - * @author sabrinechelbi - */ -public class ActionEntity extends ContextualEntity { - - public ActionEntity(AbstractActionEntityBuilder entityBuilder) { - super(entityBuilder); - this.addType(entityBuilder.type.getName()); - this.addProperty("name", entityBuilder.name); - this.addProperty("description", entityBuilder.description); - - this.addDateTimePropertyWithExceptions("startTime", entityBuilder.startTime); - this.addDateTimePropertyWithExceptions("endTime", entityBuilder.endTime); - - entityBuilder.status.ifPresent(status -> this.addIdProperty("actionStatus", status.getId())); - this.addIdProperty("agent", entityBuilder.agent); - this.addIdListProperties("instrument", entityBuilder.instruments); - this.addIdListProperties("result", entityBuilder.results); - this.addIdListProperties("object", entityBuilder.objects); - } - - abstract static class AbstractActionEntityBuilder> - extends AbstractContextualEntityBuilder { - - protected ActionType type; - protected String description; - protected String name; - protected String startTime; - protected String endTime; - protected String agent; - protected Optional status = Optional.empty(); - - protected Collection objects = new HashSet<>(); - protected Collection results = new HashSet<>(); - protected Collection instruments = new HashSet<>(); - - protected AbstractActionEntityBuilder(ActionType type) { - this.type = type; - } - - public T setDescription(String description) { - this.description = description; - return self(); - } - - public T setName(String name) { - this.name = name; - return self(); - } - - public T setStartTime(String startTime) { - this.startTime = startTime; - return self(); - } - - public T setEndTime(String endTime) { - this.endTime = endTime; - return self(); - } - - public T setAgent(String agent) { - this.agent = agent; - return self(); - } - - public T setStatus(ActionStatus status) { - this.status = Optional.ofNullable(status); - return self(); - } - - /** - * Same as calling {@link #addObject(String)} with each element of the - * collection. - * - * From the specification: "A curation Action MUST have at least one object - * which associates it with either the root data entity Dataset or one of its - * components." - * - * @param objects see {@link #addObject(String)} - * @return this builder - */ - public T addObjects(Collection objects) { - this.objects.addAll(objects); - return self(); - } - - /** - * Adds a object to the collection of objects of this ActionEntity. - * - * From the specification: "A curation Action MUST have at least one object - * which associates it with either the root data entity Dataset or one of its - * components." - * - * @param object the object to add to this ActionEntity. Duplicates will be - * ignored/removed. "The object upon which the action is carried - * out, whose state is kept intact or changed. Also known as the - * semantic roles patient, affected or undergoer (which change - * their state) or theme (which doesn't). E.g. John read a book." - * (Schema.org definition) - * @return this builder - */ - public T addObject(String object) { - this.objects.add(object); - return self(); - } - - /** - * Same as calling {@link #addResult(String)} with each element of the - * collection. - * - * @param results see {@link #addResult(String)} - * @return this builder - */ - public T addResults(Collection results) { - this.results.addAll(results); - return self(); - } - - /** - * Adds a result to the collection of results of this ActionEntity. - * - * @param result the result to add to this ActionEntity. Duplicates will be - * ignored/removed. "The result produced in the action. E.g. John - * wrote a book." (Schema.org definition) - * @return this builder - */ - public T addResult(String result) { - this.results.add(result); - return self(); - } - - /** - * Same as calling {@link #addInstrument(String)} with each element of the - * collection. - * - * @param instruments see {@link #addInstrument(String)} - * @return this builder - */ - public T addInstruments(Collection instruments) { - this.instruments.addAll(instruments); - return self(); - } - - /** - * Adds a instrument to the collection of instruments of this ActionEntity. - * - * @param instrument the instrument to add to this ActionEntity. Duplicates will - * be ignored/removed. "The object that helped the agent - * perform the action. E.g. John wrote a book with a pen." - * (Schema.org definition) - * @return this builder - */ - public T addInstrument(String instrument) { - this.instruments.add(instrument); - return self(); - } - - @Override - public abstract ActionEntity build(); - } - - public static final class ActionEntityBuilder - extends AbstractActionEntityBuilder { - - public ActionEntityBuilder(ActionType type) { - super(type); - } - - @Override - public ActionEntityBuilder self() { - return this; - } - - @Override - public ActionEntity build() { - return new ActionEntity(this); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java deleted file mode 100644 index f107dcac..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java +++ /dev/null @@ -1,18 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.contextual; - -public enum ActionStatus { - ACTIVE_ACTION_STATUS("http://schema.org/ActiveActionStatus"), - COMPLETED_ACTION_STATUS("http://schema.org/CompletedActionStatus"), - FAILED_ACTION_STATUS("http://schema.org/FailedActionStatus"), - POTENTIAL_ACTION_STATUS("http://schema.org/PotentialActionStatus"); - - private final String id; - - ActionStatus(String id) { - this.id = id; - } - - public String getId() { - return id; - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java deleted file mode 100644 index 02cff8dd..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java +++ /dev/null @@ -1,21 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.contextual; - -/** - * Enumeration class representing action types. - * @author sabrinechelbi - */ -public enum ActionType { - - CREATE("CreateAction"), - UPDATE("UpdateAction"); - - private final String name; - - ActionType(String name) { - this.name = name; - } - - public String getName(){ - return name; - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java deleted file mode 100644 index 88aaf89e..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java +++ /dev/null @@ -1,95 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.contextual; - -import java.net.URI; -import java.util.HashSet; -import java.util.Set; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; - -import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.special.CrateVersion; - -public class JsonDescriptor extends ContextualEntity { - - protected static final String CONFORMS_TO = "conformsTo"; - public static final String ID = "ro-crate-metadata.json"; - - /** - * Returns a JsonDescriptor with the conformsTo value set to the latest stable - * version. - */ - public JsonDescriptor() { - super( - staticPropertiesPrefilledBuilder() - .addIdProperty(CONFORMS_TO, CrateVersion.LATEST_STABLE.conformsTo)); - } - - protected static ContextualEntityBuilder staticPropertiesPrefilledBuilder() { - return new ContextualEntity.ContextualEntityBuilder() - .setId(ID) - .addType("CreativeWork") - .addIdProperty("about", "./"); - } - - private JsonDescriptor(ContextualEntityBuilder builder) { - super(builder); - } - - /** - * Builder for the JsonDescriptor. - *

- * Defaults to the latest stable crate version and no other conformsTo values. - */ - public static final class Builder { - CrateVersion version = CrateVersion.LATEST_STABLE; - Set otherConformsToValues = new HashSet<>(); - - public Builder() { - // default - } - - public Builder(Crate crate) { - crate.getVersion().ifPresent(v -> this.version = v); - this.otherConformsToValues.addAll(crate.getProfiles()); - } - - public Builder setVersion(CrateVersion version) { - this.version = version; - return this; - } - - public Builder addConformsTo(String uri) { - this.otherConformsToValues.add(uri); - return this; - } - - public Builder addConformsTo(URI uri) { - this.otherConformsToValues.add(uri.toString()); - return this; - } - - public JsonDescriptor build() { - if (this.otherConformsToValues.isEmpty()) { - // in this case we do not need an array - ContextualEntityBuilder entityBuilder = staticPropertiesPrefilledBuilder() - .addIdProperty(CONFORMS_TO, this.version.conformsTo); - return new JsonDescriptor(entityBuilder); - } else { - // use an array to collect all values into an array of simple @id objects. - ObjectMapper mapper = MyObjectMapper.getMapper(); - ArrayNode array = mapper.createArrayNode(); - array.add( - mapper.createObjectNode().put("@id", this.version.conformsTo)); - for (String other : this.otherConformsToValues) { - array.add( - mapper.createObjectNode().put("@id", other)); - } - ContextualEntityBuilder entityBuilder = staticPropertiesPrefilledBuilder() - .addProperty(CONFORMS_TO, array); - return new JsonDescriptor(entityBuilder); - } - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java deleted file mode 100644 index 829ee9c2..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java +++ /dev/null @@ -1,187 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.data; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import edu.kit.datamanager.ro_crate.entities.AbstractEntity; -import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; -import static edu.kit.datamanager.ro_crate.special.IdentifierUtils.isUrl; - -import java.net.URI; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -/** - * The base class of every data entity. - * - * @author Nikola Tzotchev on 4.2.2022 г. - * @version 1 - */ -public class DataEntity extends AbstractEntity { - - @JsonIgnore - private Path path; - - /** - * The constructor that takes an DataEntity builder and instantiates the - * entity. - * - * @param entityBuilder the builder passed as argument. - */ - public DataEntity(AbstractDataEntityBuilder entityBuilder) { - super(entityBuilder); - if (!entityBuilder.authors.isEmpty()) { - this.addIdListProperties("author", entityBuilder.authors); - } - this.path = entityBuilder.location; - } - - /** - * Adds an author ID to the entity. - *

- * Calling this multiple times will add multiple author IDs. - * - * @param id the identifier of the author. - */ - @SuppressWarnings("unused") - public void addAuthorId(String id) { - this.addIdProperty("author", id); - } - - /** - * Gets the path of this entity. - *

- * When reading a crate, this will point to the entity's file within the crate. - * When creating a new entity or crate, it might point to a file outside the crate, - * as set by {@link DataEntityBuilder#setLocation(Path)}. - * Such a file will be copied into the crate when writing. - * - * @return the path to the file this entity represents. - */ - @JsonIgnore - public Path getPath() { - return path; - } - - abstract static class AbstractDataEntityBuilder> extends - AbstractEntityBuilder { - - private Path location; - private List authors = new ArrayList<>(); - - /** - * Sets the location of the data entity to make a copy from when writing the crate. - *

- * Use this if you want to copy the associated file to the crate. - * Do not use it if the file should already be considered (e.g. - * as part of a DataSetEntity). - *

- * If the ID has not been set manually in beforehand, it will be derived - * from the path. Use {@link #setId(String)} to override it or set it in - * beforehand. Note that another call of this method will - * not override the ID as it has been set by the previous call! - * - * @param path the location of the data. May be null, in which case - * nothing happens. - * @return this builder - */ - public T setLocation(Path path) { - if (path != null) { - if (this.getId() == null) { - this.setId(path.getFileName().toString()); - } - this.location = path; - } - return self(); - } - - /** - * A variant of {@link #setLocation(Path)} which may throw exceptions. - * - * @param path the location of the data - * @return this builder - * @throws IllegalArgumentException if path is null - */ - public T setLocationWithExceptions(Path path) throws IllegalArgumentException { - if (path == null) { - throw new IllegalArgumentException("The given path should not be null."); - } - return setLocation(path); - } - - /** - * Same as {@link #setLocation(Path)} but instead of associating this - * entity with a file, it will point to some place on the internet. - *

- * Via the specification, this means the uri will be set as the ID. This - * call is therefore equivalent to {@link #setId(String)}. - * - * @param uri the URI, should point at the data reachable on the - * internet. - * @return this builder - */ - public T setLocation(URI uri) { - if (uri != null && this.getId() == null) { - this.setId(uri.toString()); - } - return self(); - } - - /** - * A variant of {@link #setLocation(URI)} which may throw exceptions. - * - * @param uri the given uri - * @return this builder - * @throws IllegalArgumentException if uri is null or not a valid URL - */ - public T setLocationWithExceptions(URI uri) throws IllegalArgumentException { - if (!isUrl(uri.toString())) { - throw new IllegalArgumentException("This Data Entity remote ID does not resolve to a valid URL."); - } - return setLocation(uri); - } - - public T setLicense(String id) { - this.addIdProperty("license", id); - return self(); - } - - public T setLicense(ContextualEntity license) { - this.addIdProperty("license", license.getId()); - return self(); - } - - public T addAuthor(String id) { - this.authors.add(id); - return self(); - } - - public T setContentLocation(String id) { - this.addIdProperty("contentLocation", id); - return self(); - } - - @Override - public abstract DataEntity build(); - } - - /** - * Data Entity builder class that allows for easier data entity creation. - *

- * If not explicitly mentioned, all methods avoid Exceptions and will - * silently ignore null-parameters, in which case nothing will happen. Use - * the available *WithExceptions-methods in case you need them. - */ - public static final class DataEntityBuilder extends AbstractDataEntityBuilder { - - @Override - public DataEntityBuilder self() { - return this; - } - - @Override - public DataEntity build() { - return new DataEntity(this); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java deleted file mode 100644 index 8030c04c..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java +++ /dev/null @@ -1,146 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.data; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.entities.serializers.HasPartSerializer; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * A helping class for the creating of Data entities of type Dataset. - * - * @author Nikola Tzotchev on 5.2.2022 г. - * @version 1 - */ -public class DataSetEntity extends DataEntity { - - public static final String TYPE = "Dataset"; - - /** - * Points to the parts of this dataset. - *

- * This will be serialized to and deserialized from the "hasPart" property - * and exists for convenience to represent the additional capabilities of - * a DataSetEntity over a normal DataEntity. - */ - @JsonSerialize(using = HasPartSerializer.class) - @JsonInclude(JsonInclude.Include.NON_EMPTY) - public Set hasPart; - - /** - * Constructor for instantiating a Dataset entity from the builder. - * - * @param entityBuilder the builder passed as argument. - */ - public DataSetEntity(AbstractDataSetBuilder entityBuilder) { - super(entityBuilder); - this.hasPart = entityBuilder.hasPart.stream() - .filter(s -> !s.isBlank()) - .collect(Collectors.toSet()); - this.addType(TYPE); - } - - public void removeFromHasPart(String str) { - this.hasPart.remove(str); - } - - public void addToHasPart(String id) { - if (id != null && !id.isEmpty()) { - this.hasPart.add(id); - } - } - - /** - * Check if the hasPart property contains a specific id. - * - * @deprecated use {@link #hasPart(String)} instead. - * - * @param id the id to check for - * @return true if the id is present, false otherwise - */ - @Deprecated(forRemoval = true) - public boolean hasInHasPart(String id) { - return this.hasPart.contains(id); - } - - /** - * Check if the hasPart property contains a specific id. - * @param id the id to check for - * @return true if the id is present, false otherwise - */ - public boolean hasPart(String id) { - return this.hasPart.contains(id); - } - - abstract static class AbstractDataSetBuilder> extends - AbstractDataEntityBuilder { - - Set hasPart; - - public AbstractDataSetBuilder() { - this.hasPart = new HashSet<>(); - } - - public T setHasPart(Set hasPart) { - this.hasPart = hasPart; - return self(); - } - - public T addToHasPart(DataEntity dataEntity) { - if (dataEntity != null) { - this.hasPart.add(dataEntity.getId()); - this.relatedItems.add(dataEntity.getId()); - } - return self(); - } - - public T addToHasPart(String dataEntity) { - if (dataEntity != null) { - this.hasPart.add(dataEntity); - this.relatedItems.add(dataEntity); - } - return self(); - } - - @Override - public T setAllUnsafe(ObjectNode properties) { - super.setAllUnsafe(properties); - JsonNode hasPart = properties.path("hasPart"); - String txt = hasPart.asText(); - if (!txt.isBlank()) { - this.hasPart.add(txt); - } - hasPart.valueStream() - .map(JsonNode::asText) - .filter(value -> !value.isBlank()) - .forEach( - value -> this.hasPart.add(value) - ); - return self(); - } - - @Override - public abstract DataSetEntity build(); - } - - /** - * Builder for the helping class DataSetEntity. - */ - public static final class DataSetBuilder extends AbstractDataSetBuilder { - - @Override - public DataSetBuilder self() { - return this; - } - - @Override - public DataSetEntity build() { - return new DataSetEntity(this); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/HasPartSerializer.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/HasPartSerializer.java deleted file mode 100644 index d8189958..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/HasPartSerializer.java +++ /dev/null @@ -1,48 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.serializers; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import java.io.IOException; -import java.util.Set; - -/** - * Class for serializing the hasPart property of a entity. - */ -public class HasPartSerializer extends StdSerializer> { - - public HasPartSerializer() { - this(null); - } - - public HasPartSerializer(Class> t) { - super(t); - } - - @Override - public boolean isEmpty(SerializerProvider provider, Set value) { - return value.size() == 0; - } - - @Override - public void serialize(Set value, JsonGenerator gen, SerializerProvider provider) - throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - if (value.size() > 1) { - gen.writeStartArray(); - for (String str : value) { - ObjectNode node = objectMapper.createObjectNode(); - node.put("@id", str); - gen.writeObject(node); - } - gen.writeEndArray(); - } else if (value.size() == 1) { - gen.writeObject(objectMapper.createObjectNode().put("@id", value.iterator().next())); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/ObjectNodeSerializer.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/ObjectNodeSerializer.java deleted file mode 100644 index d9ca1245..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/ObjectNodeSerializer.java +++ /dev/null @@ -1,78 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.serializers; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; - -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; - -/** - * Serialization class used with jackson to serialize the entity. - * - * @author Nikola Tzotchev on 4.2.2022 г. - * @version 1 - */ -public class ObjectNodeSerializer extends StdSerializer { - - public ObjectNodeSerializer() { - this(null); - } - - protected ObjectNodeSerializer(Class t) { - super(t); - } - - @Override - public void serialize(ObjectNode value, JsonGenerator jgen, SerializerProvider provider) - throws IOException { - - JsonNode node = JsonUtilFunctions.unwrapSingleArray(value); - final Iterator> fields = node.fields(); - - while (fields.hasNext()) { - Map.Entry field = fields.next(); - final String fieldName = field.getKey(); - JsonNode fieldValue = field.getValue(); - if (fieldValue.isObject() && fieldValue.size() == 0) { - continue; - } - if (fieldValue.isNull()) { - continue; - } - // if the type array contains only one type set it as String - // if (fieldName.equals("@type")) { - if (fieldValue.isArray()) { - if (fieldValue.isEmpty()) { - continue; - } - ArrayNode arrayNode = (ArrayNode) fieldValue; - int size = arrayNode.size(); - for (int i = size - 1; i >= 0; i--) { - var element = arrayNode.get(i); - if (element.isObject() && element.isEmpty()) { - arrayNode.remove(i); - } - } - if (arrayNode.isEmpty()) { - continue; - } - } - jgen.writeFieldName(fieldName); - jgen.writeTree(fieldValue); - } - } - - @Override - public boolean isUnwrappingSerializer() { - return true; - } - -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java deleted file mode 100644 index 18e9624a..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java +++ /dev/null @@ -1,86 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.validation; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion; -import com.networknt.schema.ValidationMessage; - -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Objects; -import java.util.Set; - -/** - * Implementation of the entity validation strategy that uses json schema. - */ -public class JsonSchemaValidation implements EntityValidationStrategy { - - private static final URL entitySchemaDefault - = Objects.requireNonNull(JsonSchemaValidation.class.getClassLoader() - .getResource("json_schemas/entity_schema.json")); - private static final URL fieldSchemaDefault - = Objects.requireNonNull(JsonSchemaValidation.class.getClassLoader() - .getResource("json_schemas/entity_field_structure_schema.json")); - - private JsonSchema entitySchema; - private JsonSchema entityFieldSchema; - - /** - * Default constructor that uses the default schemas. - */ - public JsonSchemaValidation() { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); - try { - this.entitySchema = factory.getSchema(entitySchemaDefault.toURI()); - this.entityFieldSchema = factory.getSchema(fieldSchemaDefault.toURI()); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } - - /** - * Constructor that uses custom schemas present as Json files. - * - * @param entitySchema schema for the entities validation. - * @param fieldSchema schema for the field validation. - */ - public JsonSchemaValidation(JsonNode entitySchema, JsonNode fieldSchema) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); - this.entitySchema = factory.getSchema(entitySchema); - this.entityFieldSchema = factory.getSchema(fieldSchema); - } - - @Override - public boolean validateEntity(JsonNode entity) { - Set errors = this.entitySchema.validate(entity); - if (errors.size() != 0) { - System.err.println("This entity does not comply to the basic RO-Crate entity structure."); - errors.forEach(error -> System.err.println(error.getMessage())); - return false; - } - return true; - } - - @Override - public boolean validateFieldOfEntity(JsonNode field) { - Set errors = this.entityFieldSchema.validate(field); - if (!errors.isEmpty()) { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - System.err.println("The property: "); - try { - System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(field)); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } - System.err.println("does not comply with the flattened structure" - + " of the RO-Crate json document."); - return false; - } - return true; - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java deleted file mode 100644 index 350fdad6..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java +++ /dev/null @@ -1,85 +0,0 @@ -package edu.kit.datamanager.ro_crate.externalproviders.organizationprovider; - -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.entities.contextual.OrganizationEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import java.io.IOException; -import java.util.ArrayList; - -import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class providing possibility to import organization entities from ror.com. - */ -public class RorProvider { - - private static final Logger logger = LoggerFactory.getLogger(RorProvider.class); - - private RorProvider() { - } - - /** - * The method that parses a ror entry to a crate entity. - * - * @param url the url of the ror entry. - * @return the created Organization entity. - */ - public static OrganizationEntity getOrganization(String url) { - if (!url.startsWith("https://ror.org/")) { - throw new IllegalArgumentException("Should provide ror url"); - } - String newUrl = "https://api.ror.org/v2/organizations/" + url.replaceAll("https://ror.org/", ""); - HttpGet request = new HttpGet(newUrl); - - try ( - CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(request)) { - boolean isError = response.getStatusLine().getStatusCode() != HttpStatus.SC_OK; - if (isError) { - String errorMessage = String.format("Identifier not found: %s", response.getStatusLine().toString()); - logger.error(errorMessage); - return null; - } - ObjectNode jsonNode = MyObjectMapper.getMapper().readValue(response.getEntity().getContent(), - ObjectNode.class); - - return new OrganizationEntity.OrganizationEntityBuilder() - .setId(jsonNode.path("id").asText()) - .addProperty("name", getOrganizationNameV2(jsonNode.path("names"))) - .addProperty("email", jsonNode.path("email_address")) - .addProperty("url", jsonNode.path("id").asText()) - .build(); - } catch (IOException e) { - String errorMessage = String.format("IO error: %s", e.getMessage()); - logger.error(errorMessage); - } - return null; - } - - private static String getOrganizationNameV2(JsonNode node) { - if (node.isArray()) { - for (JsonNode n : node) { - if (n.has("types") && n.path("types").isArray()) { - ArrayList l = new ObjectMapper().convertValue(n.path("types"), ArrayList.class); - if (l.contains("ror_display")) { - return n.path("value").asText(); - } - } - } - //fallback - return node.path(0).path("value").asText(); - } else { - return node.asText(); - } - } - -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java deleted file mode 100644 index 46a278c7..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java +++ /dev/null @@ -1,208 +0,0 @@ -package edu.kit.datamanager.ro_crate.hierarchy; - -import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; -import edu.kit.datamanager.ro_crate.util.FileSystemUtil; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -public class HierarchyRecognition { - protected final Crate crate; - protected final HierarchyRecognitionConfig config; - - public HierarchyRecognition(Crate crate, HierarchyRecognitionConfig config) { - this.crate = crate; - this.config = config; - } - - public HierarchyRecognitionResult buildHierarchy() { - HierarchyRecognitionResult result = new HierarchyRecognitionResult(); - - try { - // Get all data entities to process - Set allEntities = this.crate.getAllDataEntities(); - allEntities.add(crate.getRootDataEntity()); - - // Filter entities with file path IDs (not URLs, DOIs, etc.) - Map pathEntities = new HashMap<>(); - for (DataEntity entity : allEntities) { - String id = entity.getId(); - if (FileSystemUtil.isFilePath(id)) { - pathEntities.put(id, entity); - } else { - result.addSkippedEntity(entity); - } - } - - - // Validate hierarchy before making changes - if (!HierarchyRecognition.validateHierarchy(pathEntities, result)) { - return result; - } - - // Create missing intermediate entities if configured - if (config.createMissingIntermediateEntities()) { - this.createMissingIntermediateEntities(pathEntities, result); - } - - // Clear existing relationships if configured - if (config.removeExistingConnections()) { - this.clearExistingRelationships(pathEntities); - } - - // Build hierarchy relationships - this.buildHierarchyRelationships(pathEntities, config, result); - - return result; - } catch (Exception e) { - result.addError( - "Unexpected error during hierarchy recognition: " + - e.getMessage() - ); - return result; - } - } - - /** - * Validates that the hierarchy is consistent (no files containing other files/folders). - * - * @param pathEntities map of path IDs to DataEntities - * @param result builder to collect errors - * @return true if valid, false if invalid hierarchy detected - */ - protected static boolean validateHierarchy( - Map pathEntities, - HierarchyRecognitionResult result - ) { - for (Map.Entry entry : pathEntities.entrySet()) { - String childId = entry.getKey(); - String parentPath = FileSystemUtil.getParentPath(childId); - if (parentPath == null || parentPath.equals("./")) { - continue; - } - - // Check both with and without trailing slash since files don't have slash but folders do - DataEntity parentEntity = pathEntities.get(parentPath); - if (parentEntity == null) { - parentEntity = pathEntities.get(parentPath + "/"); - } - - if (parentEntity == null) { - continue; - } - - // Check for invalid hierarchy: file cannot contain another file/folder - if (parentEntity.getTypes().contains("File")) { - result.addError( - "Invalid hierarchy: file '" + - parentEntity.getId() + - "' cannot contain '" + - childId + - "'" - ); - return false; - } - } - return true; - } - - /** - * Creates missing intermediate DataSetEntity instances for folder paths. - * - * @param pathEntities map of path IDs to DataEntities - * @param result builder to collect created entities - */ - protected void createMissingIntermediateEntities( - Map pathEntities, - HierarchyRecognitionResult result - ) { - Set missingPaths = new HashSet<>(); - - // Find all missing intermediate paths - for (String path : pathEntities.keySet()) { - String parentPath = FileSystemUtil.getParentPath(path); - while (parentPath != null && !parentPath.equals("./")) { - String folderPath = parentPath + "/"; - final boolean containsParent = pathEntities.containsKey(parentPath); - final boolean containsFolder = pathEntities.containsKey(folderPath); - if (!containsParent && !containsFolder) { - missingPaths.add(folderPath); - } - parentPath = FileSystemUtil.getParentPath(parentPath); - } - } - - // Create missing DataSetEntity instances - for (String missingPath : missingPaths) { - DataSetEntity newEntity = new DataSetEntity.DataSetBuilder() - .setId(missingPath) - .addProperty("name", "Auto-generated folder: " + missingPath) - .build(); - - this.crate.addDataEntity(newEntity); - pathEntities.put(missingPath, newEntity); - result.addCreatedEntity(newEntity); - } - } - - protected void buildHierarchyRelationships( - Map pathEntities, - HierarchyRecognitionConfig config, - HierarchyRecognitionResult result - ) { - for (Map.Entry entry : pathEntities.entrySet()) { - String childId = entry.getKey(); - DataEntity childEntity = entry.getValue(); - String parentPath = FileSystemUtil.getParentPath(childId); - if (parentPath == null) { - continue; - } - - // Check both with and without trailing slash since files don't have slash but folders do - DataEntity parentEntity = pathEntities.get(parentPath); - String actualParentId = parentPath; - - if (parentEntity == null) { - parentEntity = pathEntities.get(parentPath + "/"); - actualParentId = parentPath + "/"; - } - - if (parentEntity == null) { - continue; - } - - // Add hasPart relationship - if (parentEntity instanceof DataSetEntity) { - ((DataSetEntity) parentEntity).addToHasPart(childId); - result.addProcessedRelationship( - actualParentId, - childId - ); - } - - // Add isPartOf relationship if configured - if (config.createInverseRelationships()) { - childEntity.addProperty("isPartOf", actualParentId); - } - - // Remove from root if it has a parent that is not root - if (!parentPath.equals("./")) { - this.crate.getRootDataEntity().removeFromHasPart(childId); - } - } - } - - protected void clearExistingRelationships( - Map pathEntities - ) { - for (DataEntity entity : pathEntities.values()) { - if (entity instanceof DataSetEntity) { - ((DataSetEntity) entity).hasPart.clear(); - } - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java deleted file mode 100644 index c37372d0..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.kit.datamanager.ro_crate.hierarchy; - -/** - * Configuration class for automatic hierarchy recognition. - * This class provides control over how the hierarchy recognition. - * - *

    - *
  • createMissingIntermediateEntities: Whether missing intermediate folder entities should be automatically created. Default: false
  • - *
  • createInverseRelationships: Whether isPartOf relationships should be added in addition to hasPart. Default: false
  • - *
  • removeExistingConnections: Whether hasPart relationships should be added (false) or remove existing relations in beforehand (true). Default: false
  • - *
- */ -public record HierarchyRecognitionConfig( - boolean createMissingIntermediateEntities, - boolean createInverseRelationships, - boolean removeExistingConnections -) { - /** - * Creates a new configuration with default values. - *

- * Default values: - *

    - *
  • createMissingIntermediateEntities: false
  • - *
  • createInverseRelationships: false
  • - *
  • removeExistingConnections: false
  • - *
- */ - public HierarchyRecognitionConfig() { - this(false, false, false); - } - - public HierarchyRecognitionConfig withCreateMissingIntermediateEntities(boolean value) { - return new HierarchyRecognitionConfig(value, this.createInverseRelationships, this.removeExistingConnections); - } - - public HierarchyRecognitionConfig withSetInverseRelationships(boolean value) { - return new HierarchyRecognitionConfig(this.createMissingIntermediateEntities, value, this.removeExistingConnections); - } - - public HierarchyRecognitionConfig withRemoveExistingConnections(boolean value) { - return new HierarchyRecognitionConfig(this.createMissingIntermediateEntities, this.createInverseRelationships, value); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java deleted file mode 100644 index ce4eab50..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java +++ /dev/null @@ -1,51 +0,0 @@ -package edu.kit.datamanager.ro_crate.hierarchy; - -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; - -import java.util.*; - -/** - * Result class containing information about the automatic hierarchy recognition operation. - * This class provides details about what was processed, created, and any issues encountered - * during the hierarchy recognition process. Always contains complete information about the - * operation result, including success/failure and any errors encountered. - */ -public record HierarchyRecognitionResult( - Set createdEntities, - Map> processedRelationships, - Set skippedEntities, - List warnings, - List errors -) { - HierarchyRecognitionResult() { - this(new HashSet<>(), new HashMap<>(), new HashSet<>(), new ArrayList<>(), new ArrayList<>()); - } - - /** - * Whether there were no errors during the hierarchy recognition operation. - * - * @return true if the operation completed successfully (no errors), false otherwise. - */ - public boolean isSuccessful() { - return this.errors.isEmpty(); - } - - public void addSkippedEntity(DataEntity entity) { - this.skippedEntities.add(entity); - } - - public void addError(String errorMessage) { - this.errors.add(errorMessage); - } - - public void addCreatedEntity(DataSetEntity newEntity) { - this.createdEntities.add(newEntity); - } - - public void addProcessedRelationship(String from, String to) { - this.processedRelationships - .computeIfAbsent(from, k -> new HashSet<>()) - .add(to); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/objectmapper/MyObjectMapper.java b/src/main/java/edu/kit/datamanager/ro_crate/objectmapper/MyObjectMapper.java deleted file mode 100644 index 7e711848..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/objectmapper/MyObjectMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package edu.kit.datamanager.ro_crate.objectmapper; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * Singleton pattern for the object mapper used everywhere in the library. - * - * @author Nikola Tzotchev on 4.2.2022 г. - * @version 1 - */ -public class MyObjectMapper { - - private static final ObjectMapper mapper = new ObjectMapper() - .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED) - .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); - - private MyObjectMapper() { - - } - - public static ObjectMapper getMapper() { - return mapper; - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java deleted file mode 100644 index c403ab1c..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java +++ /dev/null @@ -1,89 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import net.lingala.zip4j.ZipFile; -import net.lingala.zip4j.exception.ZipException; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import org.apache.commons.io.FileUtils; - -/** - * The default preview should use the - * rochtml tool - * for creating a simple preview file. - * - * @author Nikola Tzotchev on 6.2.2022 г. - * @version 1 - */ -public class AutomaticPreview implements CratePreview { - - public AutomaticPreview() { - } - - @Override - public void saveAllToZip(ZipFile zipFile) throws IOException { - if (PreviewGenerator.isRochtmlAvailable()) { - // extract the .json file so we can run the "rochtml" tool on it" - try { - try { - zipFile.extractFile("ro-crate-metadata.json", "temp"); - } catch (ZipException ex) { - throw new IOException("ro-crate-metadata.json not found in provided ZIP.", ex); - } - if (PreviewGenerator.isRochtmlAvailable()) { - PreviewGenerator.generatePreview("temp"); - zipFile.addFile("temp/ro-crate-preview.html"); - } - } finally { - try { - FileUtils.deleteDirectory(new File("temp")); - } catch (IOException ex) { - //ignore - } - } - } else { - new CustomPreview().saveAllToZip(zipFile); - } - } - - @Override - public void saveAllToFolder(File folder) throws IOException { - if (folder == null || !folder.exists()) { - throw new IOException("Preview target folder " + folder + " does not exist."); - } - - if (PreviewGenerator.isRochtmlAvailable()) { - PreviewGenerator.generatePreview(folder.getAbsolutePath()); - } else { - new CustomPreview().saveAllToFolder(folder); - } - } - - @Override - public void saveAllToStream(String metadata, ZipOutputStream stream) throws IOException { - if (PreviewGenerator.isRochtmlAvailable()) { - try { - FileUtils.forceMkdir(new File("temp")); - try (FileWriter writer = new FileWriter("temp/ro-crate-metadata.json")) { - writer.write(metadata); - writer.flush(); - } - if (PreviewGenerator.isRochtmlAvailable()) { - PreviewGenerator.generatePreview("temp"); - ZipStreamUtil.addFileToZipStream(stream, new File("temp/ro-crate-preview.html"), "ro-crate-preview.html"); - } - } finally { - try { - FileUtils.deleteDirectory(new File("temp")); - } catch (IOException ex) { - //ignore - } - } - } else { - new CustomPreview().saveAllToStream(metadata, stream); - } - } - -} 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 deleted file mode 100644 index e966b4af..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java +++ /dev/null @@ -1,102 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; - -import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.writer.CrateWriter; -import edu.kit.datamanager.ro_crate.writer.WriteFolderStrategy; -import net.lingala.zip4j.ZipFile; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import org.apache.commons.io.FileUtils; -import org.slf4j.LoggerFactory; - -/** - * Interface for the ROCrate preview. This manages the human-readable - * representation of a crate. - * - * @author Nikola Tzotchev on 6.2.2022 г. - * @author jejkal - * @version 2 - */ -public interface CratePreview { - - /** - * Generate a preview of the crate and store it into the given target directory. - * It is the caller's responsibility to handle, e.g. delete after use, the result - * (The caller takes ownership of the result). - *

- * IMPORTANT NOTE: This method currently has a default implementation that relies - * on deprecated methods. In future, you will have to implement this method directly. - * - * @param crate the crate to generate a preview for. - * @param targetDir the target directory to store the preview in, - * owned by the caller. - * @throws IOException if an error occurs while generating the preview. - */ - default void generate(Crate crate, File targetDir) throws IOException { - // disable preview generation to avoid recursion, - // 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(null) - .save(crate, targetDir.getAbsolutePath()); - this.saveAllToFolder(targetDir); - try (var stream = Files.list(targetDir.toPath())) { - stream - .filter(path -> !path.getFileName().toString().equals("ro-crate-preview.html")) - .filter(path -> !path.getFileName().toString().equals("ro-crate-preview_files")) - .forEach(path -> { - try { - if (Files.isDirectory(path)) { - FileUtils.deleteDirectory(path.toFile()); - } else { - Files.delete(path); - } - } catch (IOException e) { - // Silently ignore deletion errors - LoggerFactory.getLogger(CratePreview.class) - .error("Failed to delete temporary file {}", path, e); - } - }); - } - } - - /** - * Takes a crate in form of a zip file and generates a preview of it, - * which will be stored within the crate. - * - * @param zipFile the zip file with the crate, which should receive a preview. - * @throws IOException if an error occurs while saving the preview - * - * @deprecated Use {@link #generate(Crate, File)} instead. - */ - @Deprecated(since = "2.1.0", forRemoval = true) - void saveAllToZip(ZipFile zipFile) throws IOException; - - /** - * Saves the preview, given by the folder, into the given folder. - * - * @param folder the folder (containing a crate) to save the preview in. - * @throws IOException if an error occurs while saving the preview. - * - * @deprecated Use {@link #generate(Crate, File)} instead. - */ - @Deprecated(since = "2.1.0", forRemoval = true) - void saveAllToFolder(File folder) throws IOException; - - /** - * Saves the preview, given by the metadata, into the given stream. - * - * @param metadata the metadata of the crate to save the preview in. - * @param stream the stream to save the preview in. - * @throws IOException if an error occurs while saving the preview. - * - * @deprecated Use {@link #generate(Crate, File)} instead. - */ - @Deprecated(since = "2.1.0", forRemoval = true) - void saveAllToStream(String metadata, ZipOutputStream stream) throws IOException; - -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java deleted file mode 100644 index 77300a87..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java +++ /dev/null @@ -1,217 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; -import freemarker.template.TemplateExceptionHandler; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import net.lingala.zip4j.ZipFile; -import net.lingala.zip4j.exception.ZipException; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class generates a custom preview without requiring external - * dependencies, i.e., rochtml. Therefore, the FreeMarker template located under - * resources/templates/custom_preview.ftl is used. - * - * @author jejkal - */ -public class CustomPreview implements CratePreview { - - private final static Logger logger = LoggerFactory.getLogger(CustomPreview.class); - - private Template template = null; - - public CustomPreview() { - Configuration cfg = new Configuration(Configuration.VERSION_2_3_34); - cfg.setClassForTemplateLoading(CustomPreview.class, "/"); - cfg.setDefaultEncoding("UTF-8"); - cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - try { - template = cfg.getTemplate("templates/custom_preview.ftl"); - } catch (IOException ex) { - logger.error("Failed to read template for CustomPreview.", ex); - } - } - - private CustomPreviewModel mapFromJson(String metadata) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - JsonNode root = mapper.readValue(metadata, JsonNode.class); - JsonNode graph = root.get("@graph"); - CustomPreviewModel.ROCrate crate = new CustomPreviewModel.ROCrate(); - List datasets = new ArrayList<>(); - List files = new ArrayList<>(); - - if (graph.isArray()) { - - for (JsonNode node : graph) { - String id = node.path("@id").asText(); - List types = new LinkedList<>(); - if (node.path("@type").isArray()) { - - Collections.addAll(types, mapper.convertValue(node.path("@type"), String[].class)); - } else { - types.add(node.path("@type").asText()); - } - - if (types.contains("Dataset") && "./".equals(id)) { - crate.name = node.path("name").asText(); - crate.description = node.path("description").asText(null); - crate.type = "Dataset"; - crate.license = node.path("license").path("@id").asText(node.path("license").asText(null)); - crate.datePublished = node.path("datePublished").asText(null); - crate.hasPart = new ArrayList<>(); - - for (JsonNode part : node.path("hasPart")) { - CustomPreviewModel.Part p = new CustomPreviewModel.Part(); - String tmpId = part.path("@id").asText(part.asText()); - p.id = tmpId; - p.name = tmpId; // Name will be replaced later - crate.hasPart.add(p); - } - } else if (types.contains("Dataset")) { - CustomPreviewModel.Dataset dataset = new CustomPreviewModel.Dataset(); - dataset.id = id; - dataset.name = node.path("name").asText(); - dataset.description = node.path("description").asText(); - datasets.add(dataset); - } else if (types.contains("File")) { - CustomPreviewModel.File file = new CustomPreviewModel.File(); - file.id = id; - file.name = node.path("name").asText(null); - file.description = node.path("description").asText(null); - file.contentSize = node.path("contentSize").asText(null); - file.encodingFormat = node.path("encodingFormat").asText(null); - files.add(file); - } - } - } - - // Update Part names using dataset and file lists - if (crate.hasPart != null) { - for (CustomPreviewModel.Part part : crate.hasPart) { - for (CustomPreviewModel.Dataset dataset : datasets) { - if (dataset.id.equals(part.id) && dataset.name != null) { - part.name = dataset.name; - } - } - for (CustomPreviewModel.File file : files) { - if (file.id.equals(part.id) && file.name != null) { - part.name = file.name; - } - } - } - } - - CustomPreviewModel model = new CustomPreviewModel(); - model.crate = crate; - model.datasets = datasets; - model.files = files; - return model; - } - - @Override - public void saveAllToZip(ZipFile zipFile) throws IOException { - if (template == null) { - throw new IOException("Preview template did not load. Unable to proceed."); - } - if (zipFile == null) { - throw new IOException("Argument zipFile must not be null."); - } - try { - zipFile.extractFile("ro-crate-metadata.json", "temp"); - } catch (ZipException ex) { - throw new IOException("ro-crate-metadata.json not found in provided ZIP.", ex); - } - - String metadata = FileUtils.readFileToString(new File("temp/ro-crate-metadata.json"), "UTF-8"); - try { - Map dataModel = new HashMap<>(); - dataModel.put("crateModel", mapFromJson(metadata)); - - try (Writer out = new OutputStreamWriter(new FileOutputStream("temp/ro-crate-preview.html"))) { - template.process(dataModel, out); - out.flush(); - } - zipFile.addFile("temp/ro-crate-preview.html"); - } catch (TemplateException ex) { - throw new IOException("Failed to generate preview.", ex); - } finally { - try { - FileUtils.deleteDirectory(new File("temp")); - } catch (IOException ex) { - //ignore - } - } - } - - @Override - public void saveAllToFolder(File folder) throws IOException { - if (template == null) { - throw new IOException("Preview template did not load. Unable to proceed."); - } - if (folder == null || !folder.exists()) { - throw new IOException("Preview target folder " + folder + " does not exist."); - } - String metadata = FileUtils.readFileToString(new File(folder, "ro-crate-metadata.json"), "UTF-8"); - try { - Map dataModel = new HashMap<>(); - dataModel.put("crateModel", mapFromJson(metadata)); - try (Writer out = new OutputStreamWriter(new FileOutputStream(new File(folder, "ro-crate-preview.html")))) { - template.process(dataModel, out); - out.flush(); - } - } catch (TemplateException ex) { - throw new IOException("Failed to generate preview.", ex); - } - } - - @Override - public void saveAllToStream(String metadata, ZipOutputStream stream) throws IOException { - if (template == null) { - throw new IOException("Preview template did not load. Unable to proceed."); - } - try { - //prepare metadata for template - Map dataModel = new HashMap<>(); - dataModel.put("crateModel", mapFromJson(metadata)); - - //prepare output folder and writer - FileUtils.forceMkdir(new File("temp")); - //load and process template - try (FileWriter writer = new FileWriter("temp/ro-crate-preview.html")) { - //load and process template - template.process(dataModel, writer); - writer.flush(); - } - - ZipStreamUtil.addFileToZipStream(stream, new File("temp/ro-crate-preview.html"), "ro-crate-preview.html"); - } catch (TemplateException ex) { - throw new IOException("Failed to generate preview.", ex); - } finally { - try { - FileUtils.deleteDirectory(new File("temp")); - } catch (IOException ex) { - //ignore - } - } - } - -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java deleted file mode 100644 index 21dbd802..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java +++ /dev/null @@ -1,126 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import java.util.List; - -/** - * - * @author jejkal - */ -public class CustomPreviewModel { - - protected ROCrate crate; - protected List datasets; - protected List files; - - public ROCrate getCrate() { - return crate; - } - - public List getDatasets() { - return datasets; - } - - public List getFiles() { - return files; - } - - public static class ROCrate { - - protected String name; - protected String description; - protected String type; - protected String license; - protected String datePublished; - protected List hasPart; - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getType() { - return type; - } - - public String getLicense() { - return license; - } - - public String getDatePublished() { - return datePublished; - } - - public List getHasPart() { - return hasPart; - } - - } - - public static class Part { - - protected String id; - protected String name; - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - } - - public static class Dataset { - - protected String id; - protected String name; - protected String description; - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - } - - public static class File { - - protected String id; - protected String name; - protected String description; - protected String contentSize; - protected String encodingFormat; - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getContentSize() { - return contentSize; - } - - public String getEncodingFormat() { - return encodingFormat; - } - - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java deleted file mode 100644 index 60e8e19e..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java +++ /dev/null @@ -1,85 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class responsible for the generation of the human-readable representation of - * the metadata. For this to work the nodejs library rochtml has to be - * installed. This can be done using: npm install -g ro-crate-html-js or npm - * install ro-crate-html-js - */ -public class PreviewGenerator { - - private static Logger LOG = LoggerFactory.getLogger(PreviewGenerator.class); - private static final String command = "rochtml"; - - public static boolean isRochtmlAvailable() { - ProcessBuilder builder = new ProcessBuilder(); - // this is the equivalent of "rochtml dir/ro-crate-metadata.json" - // check if we are running on windows or unix - String os = System.getProperty("os.name").toLowerCase(); - if (os.contains("win")) { - builder.command("cmd.exe", "/c", command); - - } else { - builder.command("sh", "-c", command); - } - Process process; - try { - process = builder.start(); - int exitVal = process.waitFor(); - return exitVal == 0; - } catch (InterruptedException | IOException ex) { - } - return false; - } - - /** - * The method that from the location of the crate generates the html file. - * - * @param location the location of the crate in the filesystem. - */ - public static void generatePreview(String location) { - if (!isRochtmlAvailable()) { - LOG.error("rochtml is not available. Please install rochtml using npm install -g ro-crate-html-js"); - return; - } - ProcessBuilder builder = new ProcessBuilder(); - // this is the equivalent of "rochtml dir/ro-crate-metadata.json" - // check if we are running on windows or unix - String os = System.getProperty("os.name").toLowerCase(); - if (os.contains("win")) { - builder.command("cmd.exe", "/c", command + " " + location + "/ro-crate-metadata.json"); - - } else { - builder.command("sh", "-c", command + " " + location + "/ro-crate-metadata.json"); - } - - Process process; - try { - process = builder.start(); - StringBuilder output = new StringBuilder(); - - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream())); - - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - reader.close(); - int exitVal = process.waitFor(); - if (exitVal != 0) { - //abnormal... - throw new Exception("failed command"); - } - } catch (Exception e) { - LOG.error("Error while generating preview: {}", e.getMessage()); - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java deleted file mode 100644 index c3a93629..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java +++ /dev/null @@ -1,72 +0,0 @@ -package edu.kit.datamanager.ro_crate.preview; - -import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; -import java.io.File; -import java.io.IOException; - -import net.lingala.zip4j.ZipFile; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import net.lingala.zip4j.model.ZipParameters; -import org.apache.commons.io.FileUtils; - -/** - * This class adds a static preview to the crate, which consists of a - * metadataHtml file and a folder containing other files required to render - * metadataHtml. It will be put unchanged to the writer output, i.e., a zip - * file, folder, or stream. - * - * @author jejkal - */ -public class StaticPreview implements CratePreview { - - private final File metadataHtml; - private final File otherFiles; - - public StaticPreview(File metadataHtml, File otherFiles) { - this.metadataHtml = metadataHtml; - this.otherFiles = otherFiles; - } - - public StaticPreview(File metadataHtml) { - this.metadataHtml = metadataHtml; - this.otherFiles = null; - } - - @Override - public void saveAllToZip(ZipFile zipFile) throws IOException { - if (this.metadataHtml != null) { - ZipParameters zipParameters = new ZipParameters(); - zipParameters.setFileNameInZip("ro-crate-preview.html"); - zipFile.addFile(this.metadataHtml, zipParameters); - } - - if (this.otherFiles != null) { - zipFile.addFolder(this.otherFiles); - zipFile.renameFile(this.otherFiles.getName() + "/", "ro-crate-preview_files/"); - } - } - - @Override - public void saveAllToFolder(File folder) throws IOException { - if (folder == null || !folder.exists()) { - throw new IOException("Preview target folder " + folder + " does not exist."); - } - - if (this.metadataHtml != null) { - File fileInCrate = folder.toPath().resolve("ro-crate-preview.html").toFile(); - FileUtils.copyFile(this.metadataHtml, fileInCrate); - } - if (this.otherFiles != null) { - File folderName = folder.toPath().resolve("ro-crate-preview_files").toFile(); - FileUtils.copyDirectory(this.otherFiles, folderName); - } - } - - @Override - public void saveAllToStream(String metadata, ZipOutputStream stream) throws IOException { - ZipStreamUtil.addFileToZipStream(stream, this.metadataHtml, "ro-crate-preview.html"); - if (this.otherFiles != null) { - ZipStreamUtil.addFolderToZipStream(stream, this.otherFiles, "ro-crate-preview_files"); - } - } -} 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 deleted file mode 100644 index c63ba21a..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ /dev/null @@ -1,362 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.RoCrate; -import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; -import edu.kit.datamanager.ro_crate.context.RoCrateMetadataContext; -import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; -import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; -import edu.kit.datamanager.ro_crate.special.IdentifierUtils; -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; -import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.validation.Validator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * This class allows reading crates from the outside into the library in order - * to inspect or modify it. - *

- * The constructor takes a strategy to support different ways of importing the - * crates. (from zip, folder, etc.). - *

- * The reader considers "hasPart" and "isPartOf" properties and considers all - * entities (in-)directly connected to the root entity ("./") as DataEntities. - * - * @param the type of the location parameter - */ -public class CrateReader { - - private static final Logger logger = LoggerFactory.getLogger(CrateReader.class); - - /** - * This is a private inner class that shall not be exposed. **Do not make it - * public or protected.** It serves only the purpose of unsafe operations - * while reading a crate and may be specific to this implementation. - */ - private static class RoCrateUnsafe extends RoCrate { - - public void addDataEntityWithoutRootHasPart(DataEntity entity) { - this.metadataContext.checkEntity(entity); - this.roCratePayload.addDataEntity(entity); - } - } - - /** - * If the number of JSON entities in the crate is larger than this number, - * parallelization will be used. - */ - private static final int PARALLELIZATION_THRESHOLD = 100; - - private static final String FILE_PREVIEW_FILES = "ro-crate-preview_files"; - private static final String FILE_PREVIEW_HTML = "ro-crate-preview.html"; - private static final String FILE_METADATA_JSON = "ro-crate-metadata.json"; - - protected static final String SPECIFICATION_PREFIX = "https://w3id.org/ro/crate/"; - - protected static final String PROP_ABOUT = "about"; - protected static final String PROP_CONTEXT = "@context"; - protected static final String PROP_CONFORMS_TO = "conformsTo"; - protected static final String PROP_GRAPH = "@graph"; - protected static final String PROP_HAS_PART = "hasPart"; - protected static final String PROP_ID = "@id"; - - private final GenericReaderStrategy strategy; - - public CrateReader(GenericReaderStrategy strategy) { - this.strategy = strategy; - } - - /** - * This function will read the location (using one of the specified - * strategies) and then build the relation between the entities. - * - * @param location the location of the ro-crate to be read - * @return the read RO-crate - * - * @throws IOException if the crate cannot be read - */ - public RoCrate readCrate(T location) throws IOException { - // get the ro-crate-metadata.json - ObjectNode metadataJson = strategy.readMetadataJson(location); - // get the content of the crate - File files = strategy.readContent(location); - - // this set will contain the files that are associated with entities - HashSet usedFiles = new HashSet<>(); - 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).markAsImported(); - } - - private RoCrate rebuildCrate(ObjectNode metadataJson, File files, HashSet usedFiles) { - Objects.requireNonNull(metadataJson, - "metadataJson must not be null – did the strategy fail to locate 'ro-crate-metadata.json'?"); - Objects.requireNonNull(files, - "files directory must not be null – check GenericReaderStrategy.readContent()"); - JsonNode context = metadataJson.get(PROP_CONTEXT); - - CrateMetadataContext crateContext = new RoCrateMetadataContext(context); - RoCrateUnsafe crate = new RoCrateUnsafe(); - crate.setMetadataContext(crateContext); - JsonNode graph = metadataJson.get(PROP_GRAPH); - - if (graph.isArray()) { - moveRootEntitiesFromGraphToCrate(crate, (ArrayNode) graph); - } - RootDataEntity root = crate.getRootDataEntity(); - boolean rootExtractionSuccessful = root != null; - - if (rootExtractionSuccessful) { - Set dataEntityIds = getDataEntityIds(root, graph); - for (JsonNode entityJson : graph) { - String eId = unpackId(entityJson); - ObjectNode properties = entityJson.deepCopy(); - boolean isDataEntity = dataEntityIds.contains(eId); - - if (isDataEntity) { - DataEntity data = null; - boolean isDataSet = properties.path("@type").asText().equals("Dataset") - || properties.path("@type").valueStream() - .anyMatch(typeString -> typeString.asText().equals("Dataset")); - if (isDataSet) { - data = new DataSetEntity.DataSetBuilder() - .setAllUnsafe(properties) - .build(); - } else { - // data entity - DataEntity.DataEntityBuilder builder = new DataEntity.DataEntityBuilder() - .setAllUnsafe(properties); - - // Handle data entities with corresponding file - checkFolderHasFile(entityJson.get(PROP_ID).asText(), files).ifPresent(file -> { - usedFiles.add(file.getPath()); - builder.setLocationWithExceptions(file.toPath()) - .setId(file.getName()); - }); - data = builder.build(); - } - crate.addDataEntityWithoutRootHasPart(data); - - } else { - // contextual entity - crate.addContextualEntity( - new ContextualEntity.ContextualEntityBuilder() - .setAllUnsafe(properties) - .build()); - } - } - } - - Collection untrackedFiles = Arrays.stream( - Optional.ofNullable(files.listFiles()).orElse(new File[0])) - .filter(f -> !usedFiles.contains(f.getPath())) - .collect(Collectors.toSet()); - - crate.setUntrackedFiles(untrackedFiles); - Validator defaultValidation = new Validator(new JsonSchemaValidation()); - defaultValidation.validate(crate); - return crate; - } - - /** - * Extracts graph connections from top to bottom. - *

- * Example: (connections.get(parent) -> children) - * - * @param graph the ArrayNode with all Entities. - * @return the graph connections. - */ - protected Map> makeEntityGraph(JsonNode graph) { - Map> connections = new HashMap<>(); - - Map idToNodes = new HashMap<>(); - StreamSupport.stream(graph.spliterator(), false) - .forEach(jsonNode -> idToNodes.put(unpackId(jsonNode), jsonNode)); - - for (JsonNode entityNode : graph) { - String currentId = unpackId(entityNode); - StreamSupport.stream(entityNode.path("hasPart").spliterator(), false) - .map(this::unpackId) - .map(s -> idToNodes.getOrDefault(s, null)) - .filter(Objects::nonNull) - .forEach(child -> connections.computeIfAbsent(currentId, key -> new HashSet<>()) - .add(unpackId(child))); - StreamSupport.stream(entityNode.path("isPartOf").spliterator(), false) - .map(this::unpackId) - .map(s -> idToNodes.getOrDefault(s, null)) - .filter(Objects::nonNull) - .forEach(parent -> connections.computeIfAbsent(unpackId(parent), key -> new HashSet<>()) - .add(currentId)); - } - return connections; - } - - protected Set getDataEntityIds(RootDataEntity root, JsonNode graph) { - if (root == null) { - return Set.of(); - } - Map> network = makeEntityGraph(graph); - Set directDataEntities = new HashSet<>(root.hasPart); - - Stack processingQueue = new Stack<>(); - processingQueue.addAll(directDataEntities); - Set result = new HashSet<>(); - - while (!processingQueue.empty()) { - String currentId = processingQueue.pop(); - result.add(currentId); - network.getOrDefault(currentId, new HashSet<>()).stream() - .filter(subId -> !result.contains(subId)) // avoid loops! - .forEach(subId -> { - result.add(subId); - processingQueue.add(subId); - }); - } - return result; - } - - protected String unpackId(JsonNode node) { - if (node.isTextual()) { - return node.asText(); - } else /*if (node.isObject())*/ { - return node.path(PROP_ID).asText(); - } - } - - protected Optional checkFolderHasFile(String filepathOrId, File folder) { - if (IdentifierUtils.isUrl(filepathOrId)) { - return Optional.empty(); - } - return Stream.of(IdentifierUtils.decode(filepathOrId).orElse(filepathOrId), filepathOrId) - .map(filename -> folder.toPath().resolve(filename).normalize().toAbsolutePath()) - // defence-in-depth: ensure we are still inside the crate folder - .filter(resolved -> resolved.startsWith(folder.toPath())) - .map(Path::toFile) - .filter(File::exists) - .findFirst(); - } - - /** - * Moves the descriptor and the root entity from the graph to the crate. - *

- * Extracts the root data entity and the Metadata File Descriptor from the - * graph and inserts them into the crate object. It also deletes it from the - * graph. We will need the root dataset to distinguish between data entities - * and contextual entities. - * - * @param crate the crate, which will receive the entities, if available in - * the graph. - * @param graph the graph of the Metadata JSON file, where the entities are - * extracted and removed from. - */ - protected void moveRootEntitiesFromGraphToCrate(RoCrate crate, ArrayNode graph) { - Optional maybeDescriptor = getMetadataDescriptor(graph); - - maybeDescriptor.ifPresent(descriptor -> { - setCrateDescriptor(crate, descriptor); - JsonUtilFunctions.removeJsonNodeFromArrayNode(graph, descriptor); - - Optional maybeRoot = extractRoot(graph, descriptor); - - maybeRoot.ifPresent(root -> { - Set hasPartIds = extractHasPartIds(root); - - crate.setRootDataEntity( - new RootDataEntity.RootDataEntityBuilder() - .setAllUnsafe(root.deepCopy()) - .setHasPart(hasPartIds) - .build()); - - JsonUtilFunctions.removeJsonNodeFromArrayNode(graph, root); - }); - }); - } - - /** - * Find the metadata descriptor. - *

- * Currently prefers algorithm of version 1.1 over the one of 1.2-DRAFT. - * - * @param graph the graph to search the descriptor in. - * @return the metadata descriptor of the crate. - */ - protected Optional getMetadataDescriptor(ArrayNode graph) { - boolean isParallel = graph.size() > PARALLELIZATION_THRESHOLD; - // use the algorithm described here: - // https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity - Optional maybeDescriptor = StreamSupport.stream(graph.spliterator(), isParallel) - // "2. if the conformsTo property is a URI that starts with - // https://w3id.org/ro/crate/" - .filter(node -> node.path(PROP_CONFORMS_TO).path(PROP_ID).asText().startsWith(SPECIFICATION_PREFIX)) - // "3. from this entity’s about object keep the @id URI as variable root" - .filter(node -> node.path(PROP_ABOUT).path(PROP_ID).isTextual()) - // There should be only one descriptor. If multiple exist, we take the first - // one. - .findFirst(); - return maybeDescriptor.or(() - -> // from https://www.researchobject.org/ro-crate/1.2-DRAFT/root-data-entity.html#finding-the-root-data-entity - StreamSupport.stream(graph.spliterator(), isParallel) - .filter(node -> node.path(PROP_ID).asText().equals(FILE_METADATA_JSON)) - .findFirst() - ); - } - - /** - * Extracts the root entity from the graph, using the information from the - * descriptor. - *

- * Basically implements step 5 of the algorithm described here: - * - * https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity - * - * - * @param graph the graph from the metadata JSON-LD file - * @param descriptor the RO-Crate descriptor - * @return the root entity, if found - */ - private Optional extractRoot(ArrayNode graph, JsonNode descriptor) { - String rootId = descriptor.get(PROP_ABOUT).get(PROP_ID).asText(); - boolean isParallel = graph.size() > PARALLELIZATION_THRESHOLD; - return StreamSupport.stream(graph.spliterator(), isParallel) - // root is an object (filter + conversion) - .filter(JsonNode::isObject) - .map(JsonNode::deepCopy) - // "5. if the entity has an @id URI that matches root return it" - .filter(node -> node.path(PROP_ID).asText().equals(rootId)) - .findFirst(); - } - - private Set extractHasPartIds(ObjectNode root) { - JsonNode hasPartNode = root.path(PROP_HAS_PART); - boolean isParallel = hasPartNode.isArray() && hasPartNode.size() > PARALLELIZATION_THRESHOLD; - Set hasPartIds = StreamSupport.stream(hasPartNode.spliterator(), isParallel) - .map(hasPart -> hasPart.path(PROP_ID).asText()) - .filter(text -> !text.isBlank()) - .collect(Collectors.toSet()); - if (hasPartIds.isEmpty() && hasPartNode.path(PROP_ID).isTextual()) { - hasPartIds.add(hasPartNode.path(PROP_ID).asText()); - } - return hasPartIds; - } - - private void setCrateDescriptor(RoCrate crate, JsonNode descriptor) { - ContextualEntity descriptorEntity = new ContextualEntity.ContextualEntityBuilder() - .setAllUnsafe(descriptor.deepCopy()) - .build(); - crate.setJsonDescriptor(descriptorEntity); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java deleted file mode 100644 index 6a9f4bd4..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.File; -import java.io.IOException; - -/** - * Generic interface for the strategy of the reader class. - * This allows for flexible input types when implementing different reading strategies. - * - * @param the type which determines the source of the crate - */ -public interface GenericReaderStrategy { - /** - * Read the metadata.json file from the given location. - * - * @param location the location to read from - * @return the parsed metadata.json as ObjectNode - */ - ObjectNode readMetadataJson(SOURCE_TYPE location) throws IOException; - - /** - * Read the content from the given location. - * - * @param location the location to read from - * @return the content as a File - */ - File readContent(SOURCE_TYPE location) throws IOException; -} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java deleted file mode 100644 index 927d77de..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java +++ /dev/null @@ -1,32 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; - -/** - * A class for reading a crate from a folder. - * - * @author Nikola Tzotchev on 9.2.2022 г. - * @version 1 - */ -public class ReadFolderStrategy implements GenericReaderStrategy { - - @Override - public ObjectNode readMetadataJson(String location) throws IOException { - Path metadata = new File(location).toPath().resolve("ro-crate-metadata.json"); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode objectNode = objectMapper.createObjectNode(); - objectNode = objectMapper.readTree(metadata.toFile()).deepCopy(); - return objectNode; - } - - @Override - public File readContent(String location) { - return new File(location); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java deleted file mode 100644 index 3cc41086..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java +++ /dev/null @@ -1,176 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Path; -import java.util.UUID; - -import edu.kit.datamanager.ro_crate.util.FileSystemUtil; -import net.lingala.zip4j.io.inputstream.ZipInputStream; -import net.lingala.zip4j.model.LocalFileHeader; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.FileFilterUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Reads a crate from a streamed ZIP archive. - *

- * This class handles reading and extraction of RO-Crate content from ZIP archives - * into a temporary directory structure on the file system, - * which allows accessing the contained files. - *

- * Supports ELN-Style crates, - * meaning the crate may be either in the zip archive directly or in a single, - * direct subfolder beneath the root folder (/folder). - *

- * Note: This implementation checks for up to 50 subdirectories if multiple are present. - * This is to avoid zip bombs, which may contain a lot of subdirectories, - * and at the same time gracefully handle valid crated with hidden subdirectories - * (for example, thumbnails). - *

- * NOTE: The resulting crate may refer to these temporary files. Therefore, - * these files are only being deleted before the JVM exits. If you need to free - * space because your application is long-running or creates a lot of - * crates, you may use the getters to retrieve information which will help - * you to clean up manually. Keep in mind that crates may refer to this - * folder after extraction. Use RoCrateWriter to export it so some - * persistent location and possibly read it from there, if required. Or use - * the ZipWriter to write it back to its source. - * - * @author jejkal - */ -public class ReadZipStreamStrategy implements GenericReaderStrategy { - - private static final Logger logger = LoggerFactory.getLogger(ReadZipStreamStrategy.class); - protected final String ID = UUID.randomUUID().toString(); - protected Path temporaryFolder = Path.of(String.format("./.tmp/ro-crate-java/zipStreamReader/%s/", ID)); - protected boolean isExtracted = false; - - /** - * Crates an instance with the default configuration. - *

- * The default configuration is to extract the ZipFile to - * `./.tmp/ro-crate-java/zipStreamReader/%UUID/`. - */ - public ReadZipStreamStrategy() {} - - /** - * Creates a ZipStreamReader which will extract the contents temporary to - * the given location instead of the default location. - * - * @param folderPath the custom directory to extract content to for - * temporary access. - * @param shallAddUuidSubfolder if true, the reader will extract into - * subdirectories of the given directory. These subdirectories will have - * UUIDs as their names. - */ - public ReadZipStreamStrategy(Path folderPath, boolean shallAddUuidSubfolder) { - if (shallAddUuidSubfolder) { - this.temporaryFolder = folderPath.resolve(ID); - } else { - this.temporaryFolder = folderPath; - } - } - - /** - * @return the identifier which may be used as the name for a subfolder in - * the temporary directory. - */ - public String getID() { - return ID; - } - - /** - * @return the folder (considered temporary) where the zipped crate will be - * or has been extracted to. - */ - public Path getTemporaryFolder() { - return temporaryFolder; - } - - /** - * @return whether the crate has already been extracted into the temporary - * folder. - */ - public boolean isExtracted() { - return isExtracted; - } - - /**Read the crate metadata and content from the provided input stream. - * - * @param stream The input stream. - */ - private void readCrate(InputStream stream) throws IOException { - File folder = temporaryFolder.toFile(); - FileSystemUtil.mkdirOrDeleteContent(folder); - - LocalFileHeader localFileHeader; - int readLen; - byte[] readBuffer = new byte[4096]; - - try (ZipInputStream zipInputStream = new ZipInputStream(stream)) { - while ((localFileHeader = zipInputStream.getNextEntry()) != null) { - String fileName = localFileHeader.getFileName(); - File extractedFile = new File(folder, fileName).getCanonicalFile(); - Path targetRoot = folder.toPath().toRealPath(); - if (!extractedFile.toPath().startsWith(targetRoot)) { - throw new IOException("Entry is outside of target directory: " + fileName); - } - if (localFileHeader.isDirectory()) { - FileUtils.forceMkdir(extractedFile); - continue; - } - FileUtils.forceMkdir(extractedFile.getParentFile()); - try (OutputStream outputStream = new FileOutputStream(extractedFile)) { - while ((readLen = zipInputStream.read(readBuffer)) != -1) { - outputStream.write(readBuffer, 0, readLen); - } - } - } - } - this.isExtracted = true; - // register deletion on exit - FileUtils.forceDeleteOnExit(folder); - } - - @Override - public ObjectNode readMetadataJson(InputStream stream) throws IOException { - if (!isExtracted) { - this.readCrate(stream); - } - - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - File jsonMetadata = temporaryFolder.resolve(JsonDescriptor.ID).toFile(); - if (!jsonMetadata.isFile()) { - // Try to find the metadata file in subdirectories - File firstSubdir = FileUtils.listFilesAndDirs( - temporaryFolder.toFile(), - FileFilterUtils.directoryFileFilter(), - null - ) - .stream() - .limit(50) - .filter(file -> file.toPath().toAbsolutePath().resolve(JsonDescriptor.ID).toFile().isFile()) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No %s found in zip file".formatted(JsonDescriptor.ID))); - jsonMetadata = firstSubdir.toPath().resolve(JsonDescriptor.ID).toFile(); - } - return objectMapper.readTree(jsonMetadata).deepCopy(); - } - - @Override - public File readContent(InputStream stream) throws IOException { - if (!isExtracted) { - this.readCrate(stream); - } - return temporaryFolder.toFile(); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java deleted file mode 100644 index 83a43701..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java +++ /dev/null @@ -1,77 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import java.io.InputStream; -import java.nio.file.Path; - -/** - * Factory for creating common RO-Crate reader instances. - * Provides convenient static methods to instantiate readers with pre-configured strategies. - */ -public class Readers { - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private Readers() {} - - /** - * Creates a reader that reads from ZIP files using input streams. - * - * @return A reader configured for ZIP files - * - * @see ReadZipStreamStrategy#ReadZipStreamStrategy() - */ - public static CrateReader newZipStreamReader() { - return new CrateReader<>(new ReadZipStreamStrategy()); - } - - /** - * Creates a reader that reads from ZIP files using input streams, - * extracting to a custom temporary location. - * - * @param extractPath Path where ZIP contents should be extracted - * @param useUuidSubfolder Whether to create a UUID subfolder under extractPath - * @return A reader configured for ZIP files with custom extraction - * - * @see ReadZipStreamStrategy#ReadZipStreamStrategy(Path, boolean) - */ - public static CrateReader newZipStreamReader(Path extractPath, boolean useUuidSubfolder) { - return new CrateReader<>(new ReadZipStreamStrategy(extractPath, useUuidSubfolder)); - } - - /** - * Creates a reader that reads from a folder using a string path. - * - * @return A reader configured for folders - * - * @see ReadFolderStrategy - */ - public static CrateReader newFolderReader() { - return new CrateReader<>(new ReadFolderStrategy()); - } - - /** - * Creates a reader that reads from a ZIP file using a string path. - * - * @return A reader configured for ZIP files - * - * @see ReadZipStrategy#ReadZipStrategy() - */ - public static CrateReader newZipPathReader() { - return new CrateReader<>(new ReadZipStrategy()); - } - - /** - * Creates a reader that reads from a ZIP file using a string path, - * extracting to a custom temporary location. - * - * @param extractPath Path where ZIP contents should be extracted - * @param useUuidSubfolder Whether to create a UUID subfolder under extractPath - * @return A reader configured for ZIP files with custom extraction - * - * @see ReadZipStrategy#ReadZipStrategy(Path, boolean) - */ - public static CrateReader newZipPathReader(Path extractPath, boolean useUuidSubfolder) { - return new CrateReader<>(new ReadZipStrategy(extractPath, useUuidSubfolder)); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java b/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java deleted file mode 100644 index 74d8ca1c..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java +++ /dev/null @@ -1,111 +0,0 @@ -package edu.kit.datamanager.ro_crate.special; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Optional; - -/** - * Represents a crate version and has information for each version available. - * - * It offers also convenience functionality like version comparison similar to - * numbers. - */ -public enum CrateVersion { - // NOTE: Java defines compareTo for enums to work in the order of the defined - // elements. This means higher versions have to be defined below the lower - // versions. Consider this when adding a new version. - // - // NOTE: To add a new version, simply append a new variant with the correct - // data and read the comment below the versions. - V1P1("https://w3id.org/ro/crate/1.1"), - V1P2_DRAFT("https://w3id.org/ro/crate/1.2-DRAFT"); - - // NOTE: Do not forget to adjust the following when adding a version: - public static final CrateVersion LATEST_STABLE = CrateVersion.V1P1; - public static final CrateVersion LATEST_UNSTABLE = CrateVersion.V1P2_DRAFT; - - /** - * The String which is referred to by the crates conformsTo value. - * Should lead to the specification when resolved as a URL. - */ - public final String conformsTo; - - /** - * The String representation of the version. - * - * Example: assertEquals("1.2-DRAFT", CrateVersion.V1P2_DRAFT); - */ - public final String version; - - /** - * Basically a constructor of a version, if it needs to be created from the spec - * URI. - * - * @param conformsTo the specification URI. Example: - * https://w3id.org/ro/crate/1.1 - * @return the matching CrateVersion enum, if the URI matches any. Empty if not. - */ - public static Optional fromSpecUri(String conformsTo) { - return Optional.ofNullable(crateVersionOfConformsTo(conformsTo)); - } - - /** - * Private constructor which is being used for the internally given information - * above. - * - * @param spec the specification URI / conformsTo value - */ - private CrateVersion(String spec) { - this.conformsTo = spec; - this.version = getVersionFromConformsToString(spec); - } - - public boolean isStable() { - return !this.version.endsWith("-DRAFT"); - } - - public URI getConformsToUri() { - try { - return new URI(this.conformsTo); - } catch (URISyntaxException e) { - // can not happen as the user can not set this string - return null; - } - } - - /** - * Extracts the version from the spec uri. - * - * @param specUri the uri of the spec - * @return the version of the spec - */ - private static String getVersionFromConformsToString(String specUri) { - String[] parts = specUri.split("/"); - return parts[parts.length - 1]; - } - - private static CrateVersion crateVersionOfConformsTo(String conformsTo) { - for (CrateVersion e : values()) { - if (e.conformsTo.equals(conformsTo)) { - return e; - } - } - return null; - } - - public boolean isGreaterThan(CrateVersion other) { - return this.compareTo(other) > 0; - } - - public boolean isGreaterOrEqualThan(CrateVersion other) { - return this.compareTo(other) >= 0; - } - - public boolean isLowerThan(CrateVersion other) { - return this.compareTo(other) < 0; - } - - public boolean isLowerOrEqualThan(CrateVersion other) { - return this.compareTo(other) <= 0; - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java b/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java deleted file mode 100644 index 4fec8e58..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java +++ /dev/null @@ -1,158 +0,0 @@ -package edu.kit.datamanager.ro_crate.special; - -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.util.Optional; - -import com.apicatalog.jsonld.uri.UriUtils; -import java.net.URI; -import java.net.URISyntaxException; - -/** - * This class defines methods regarding URIs in general, which in RO-Crate - * context means usually a valid, resolvable URL or a relative file path. - *

- * The purpose is to have a simple abstraction where the way e.g. a URL is - * checked can be changed and tested easily for the whole library. - */ -public class IdentifierUtils { - - /** Static class */ - private IdentifierUtils() {} - - /** - * Returns true, if the given String is encoded and can be used as an - * identifier in RO-Crate. - *

- * Possible identifiers include: - * - a uri - * - a url - * - a valid LD identifier (including blank node identifiers) - * - relative identifiers - * - * @param uri the given URI. Usually a URL or relative file path. - * @return true if the url is encoded, false if it is not. - */ - public static boolean isValidUri(String uri) { - return UriUtils.isURI(uri) || isLdBlankNode(uri); - } - - /** - * Returns true, if the given string is a url. - * - * @param uri the given string - * @return true if it is a url, false otherwise. - */ - public static boolean isUrl(String uri) { - try { - return encode(uri) - .map(encodedUri -> asUrl(encodedUri).isPresent()) - .orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Tests if the given String is a URL, and if so, returns it. - * - * @param uriAsString the given String which will be tested. - * @return the url, if it is one. - */ - public static Optional asUrl(String uriAsString) { - try { - URI uri = new URI(uriAsString); - if (uri.isAbsolute()) { - return Optional.of(uri.toURL()); - } - } catch (URISyntaxException | MalformedURLException | NullPointerException ex) { - return Optional.empty(); - } - return Optional.empty(); - } - - /** - * Returns true, if the given string is a file path. - * - * @param uri the given string - * @return true if it is a path, false otherwise. - */ - public static boolean isPath(String uri) { - return asPath(uri).isPresent(); - } - - /** - * Tests if the given String is a file path, and if so, returns it. - * - * @param uri the given String which will be tested. - * @return the path, if it is one. - */ - public static Optional asPath(String uri) { - try { - Path u = Path.of(uri); - if (!isUrl(uri)) { - return Optional.of(u); - } - } catch (InvalidPathException e) { - return Optional.empty(); - } - return Optional.empty(); - } - - /** - * Encodes a string using the description if the ro-crate-specification. - * - * @param uri the string to encode. - * @return the encoded version of the given string, if possible. - */ - public static Optional encode(String uri) { - String result = uri; - // according to - // https://www.researchobject.org/ro-crate/1.1/data-entities.html#encoding-file-paths - // a file path may not be fully encoded and may contain international unicode - // characters. So we try a "soft"-encoding first, and if this is not yet valid, - // we really fully encode it. - - result = result.replace("\\", "/"); // "...express any relative paths using / separator..." - result = result.replace("%", "%25"); // "...escape special characters like space (%25)..." - result = result.replace(" ", "%20"); // "...and percent (%25)..." - if (isValidUri(result)) { - return Optional.of(result); - } - // from here on, our soft-encoding failed and we will fully encode the url or - // path. - result = URLEncoder.encode(uri, StandardCharsets.UTF_8); - result = result.replace("+", "%20"); // UrlEncoder encodes spaces with "+" - result = result.replace("%3A", ":"); // Convert colons back - result = result.replace("%2F", "/"); // Convert slashes back - - if (isValidUri(result)) { - return Optional.of(result); - } else { - return Optional.empty(); - } - } - - public static Optional decode(String uri) { - if (!isValidUri(uri) || isLdBlankNode(uri)) { - return Optional.of(uri); - } - return Optional.of(URLDecoder.decode(uri, StandardCharsets.UTF_8)); - } - - /** - * Returns true if the given string is a blank node identifier in the Linked - * Data world. - * - * @param uri the given String - * @return true if the string is a blank node. False if not. - */ - private static boolean isLdBlankNode(String uri) { - return uri.startsWith("_:") && !uri.contains(" "); - } -} 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 deleted file mode 100644 index 812772cd..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -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 { - public static final String VERSION_PROPERTIES = "version.properties"; - - /** - * 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() { - // Lazy initialization - version loaded on first access - } - - @Override - public String getVersion() { - if (cachedVersion != null) { - return cachedVersion; - } - - 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; - 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/FileSystemUtil.java b/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java deleted file mode 100644 index 82c39529..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java +++ /dev/null @@ -1,122 +0,0 @@ -package edu.kit.datamanager.ro_crate.util; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.regex.Matcher; - -public class FileSystemUtil { - private FileSystemUtil() { - // Utility class, no instantiation - } - - /** - * Checks if the given ID appears to be a file path. - *

- * Specifically excludes IDs starting with "doi:", "http", or "https". - * - * @param id the ID to check - * @return true if it looks like a file path, false otherwise - */ - public static boolean isFilePath(String id) { - return id != null && !( - id.startsWith("doi:") || - id.startsWith("http://") || - id.startsWith("https://") - ); - } - - /** - * Gets the parent path of a given path. - * @param path the path to evaluate. - * @return the parent path, or null if no parent exists. - */ - public static String getParentPath(String path) { - if (path == null || path.equals("./") || path.isEmpty()) { - return null; - } - - // Normalize path - remove trailing slash for consistency - String normalizedPath = path.endsWith("/") - ? path.substring(0, path.length() - 1) - : path; - - int lastSlash = normalizedPath.lastIndexOf('/'); - if (lastSlash == -1) { - return "./"; // Root directory - } - - String parentPath = normalizedPath.substring(0, lastSlash); - - // If parent is empty, it's root - if (parentPath.isEmpty()) { - return "./"; - } - - // For validation, we need to check both with and without trailing slash - // since files don't have trailing slash but folders do - return parentPath; - } - - /** - * Removes a specific set of given file extensions from a file name, if present. - * The extensions are case-insensitive. Given "ELN", "eln" or "Eln" will also match. - * The dot (.) before the extension is also assumed and removed implicitly: - *

- * Example: - * filterExtensionsFromFileName("test.eln", Set.of("ELN")) -> "test" - * - * @param filename the file name to filter (must not be null) - * @param extensionsToRemove the extensions to remove (must not be null) - * @return the filtered file name - * @throws NullPointerException if any parameter is null - */ - public static String filterExtensionsFromFileName(String filename, Collection extensionsToRemove) { - String dot = Matcher.quoteReplacement("."); - String end = Matcher.quoteReplacement("$"); - for (String extension : extensionsToRemove) { - // (?i) removes case sensitivity - filename = filename.replaceFirst("(?i)" + dot + extension + end, ""); - } - return filename; - } - - /** - * Ensures that a given path ends with a trailing slash. - * - * @param path the path to check - * @return the path with a trailing slash if it didn't have one, or the original path - */ - public static String ensureTrailingSlash(String path) { - if (path == null || path.isEmpty()) { - return path; - } - if (!path.endsWith("/")) { - return path + "/"; - } - return path; - } - - /** - * Creates a directory or deletes its content if it already exists. - * - * @param folder the folder to create or delete content from - * @throws IOException if an I/O error occurs - */ - public static void mkdirOrDeleteContent(File folder) throws IOException { - File[] files = folder.listFiles(); - boolean isNonEmptyDir = folder.exists() - && folder.isDirectory() - && files != null - && files.length > 0; - boolean isFile = folder.exists() - && !folder.isDirectory(); - - if (isNonEmptyDir || isFile) { - FileUtils.forceDelete(folder); - } - FileUtils.forceMkdir(folder); - } -} 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 deleted file mode 100644 index 45161f16..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java +++ /dev/null @@ -1,61 +0,0 @@ -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 { - - private Graph() { - // Private constructor to prevent instantiation - } - - /** - * 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/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java deleted file mode 100644 index cd5c4ad2..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -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/util/ZipStreamUtil.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java deleted file mode 100644 index b8eb2ef1..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java +++ /dev/null @@ -1,93 +0,0 @@ -package edu.kit.datamanager.ro_crate.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import net.lingala.zip4j.model.ZipParameters; - -/** - * - * @author jejkal - */ -public class ZipStreamUtil { - - /** - * Adds a folder and its contents to a ZipOutputStream. - * - * @param zipOutputStream The ZipOutputStream to which the folder will be added. - * @param folder The folder to be added. - * @param parentPath The path in the zip file where the folder will be added. - * @throws IOException If an I/O error occurs. - */ - public static void addFolderToZipStream( - ZipOutputStream zipOutputStream, - File folder, - String parentPath - ) throws IOException { - if (!folder.exists() || !folder.isDirectory()) { - throw new IllegalArgumentException( - "The provided folder path is not a valid directory: %s" - .formatted(folder.getAbsolutePath()) - ); - } - - File[] files = folder.listFiles(); - if (files == null) { - return; - } - - for (File file : files) { - String zipEntryPath = parentPath.isEmpty() ? file.getName() : parentPath + "/" + file.getName(); - if (file.isDirectory()) { - addFolderToZipStream(zipOutputStream, file.getAbsolutePath(), zipEntryPath); - } else { - addFileToZipStream(zipOutputStream, file, zipEntryPath); - } - } - } - - /** - * Adds a folder and its contents to a ZipOutputStream. - * @param zipOutputStream The ZipOutputStream to which the folder will be added. - * @param folderPath The path of the folder to be added. - * @param parentPath The path in the zip file where the folder will be added. - * @throws IOException If an I/O error occurs. - */ - public static void addFolderToZipStream( - ZipOutputStream zipOutputStream, - String folderPath, - String parentPath - ) throws IOException { - addFolderToZipStream(zipOutputStream, new File(folderPath), parentPath); - } - - /** - * Adds a file to a ZipOutputStream. - * - * @param zipOutputStream The ZipOutputStream to which the file will be added. - * @param file The file to be added. - * @param zipEntryPath The path in the zip file where the file will be added. - * @throws IOException If an I/O error occurs. - */ - public static void addFileToZipStream( - ZipOutputStream zipOutputStream, - File file, - String zipEntryPath - ) throws IOException { - ZipParameters zipParameters = new ZipParameters(); - zipParameters.setFileNameInZip(zipEntryPath); - zipOutputStream.putNextEntry(zipParameters); - - try (InputStream inputStream = new FileInputStream(file)) { - byte[] buffer = new byte[4096]; - int len; - while ((len = inputStream.read(buffer)) != -1) { - zipOutputStream.write(buffer, 0, len); - } - } - - zipOutputStream.closeEntry(); - } -} 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 deleted file mode 100644 index d708ddd7..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ /dev/null @@ -1,55 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; - -import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.validation.Validator; - -import java.io.IOException; - -/** - * The class used for writing (exporting) crates. The class uses a strategy - * pattern for writing crates as different formats. (zip, folders, etc.) - * - * @param the type which determines the destination of the result - */ -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; - } - - /** - * 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; - } - - /** - * This method saves the crate to a destination provided. - * - * @param crate the crate to write. - * @param destination the location where the crate should be written. - */ - public void save(Crate crate, DESTINATION_TYPE destination) throws IOException { - Validator defaultValidation = new Validator(new JsonSchemaValidation()); - defaultValidation.validate(crate); - if (this.provenanceManager != null) { - this.provenanceManager.addProvenanceInformation(crate); - } - this.strategy.save(crate, destination); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java deleted file mode 100644 index bd6310cd..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java +++ /dev/null @@ -1,27 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; - -/** - * An Interface for {@link GenericWriterStrategy} implementations which support writing - * ELN-Style crates. - * - * @param the type which determines the destination of the result - */ -public interface ElnFormatWriter extends GenericWriterStrategy { - - /** - * Write in ELN format style, meaning with a root subfolder in the zip file. - * Same as {@link #withRootSubdirectory()}. - * - * @return this writer - */ - ElnFormatWriter usingElnStyle(); - - /** - * Alias with more generic name for {@link #usingElnStyle()}. - * - * @return this writer - */ - default ElnFormatWriter withRootSubdirectory() { - return this.usingElnStyle(); - } -} 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 deleted file mode 100644 index fd4a2c50..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ /dev/null @@ -1,138 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; - -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.time.Instant; - -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. - */ -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; - } - - @Override - public String toString() { - return prefix; - } - } - - /** - * 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; - - /** - * 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; - } - - /** - * 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()); - } - - /** - * 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())) - && !crate.isImported(); - - String libraryId = this.getLibraryId(); - - // Create action entity first - ContextualEntity actionEntity = buildNewActionEntity(isFirstWrite, libraryId); - - // Create or update ro-crate-java entity - ContextualEntity roCrateJavaEntity = buildRoCrateJavaEntity(crate, actionEntity.getId(), libraryId); - - // Add entities to crate - crate.addContextualEntity(roCrateJavaEntity); - crate.addContextualEntity(actionEntity); - } - - protected ContextualEntity buildNewActionEntity(boolean isFirstWrite, String libraryId) { - return new ContextualEntityBuilder() - .addType(isFirstWrite ? "CreateAction" : "UpdateAction") - .addIdProperty("result", "./") - .addProperty("startTime", Instant.now().toString()) - .addIdProperty("agent", libraryId) - .build(); - } - - protected ContextualEntity buildRoCrateJavaEntity( - Crate crate, - String newActionId, - String libraryId - ) { - String version = this.versionProvider.getVersion(); - ContextualEntity self = crate.getAllContextualEntities().stream() - .filter(contextualEntity -> libraryId.equals(contextualEntity.getId())) - .findFirst() - .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/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java deleted file mode 100644 index 0816cd59..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java +++ /dev/null @@ -1,91 +0,0 @@ -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.Crate; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.special.IdentifierUtils; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; - -/** - * A class for writing a crate to a folder. - * - * @author Nikola Tzotchev on 9.2.2022 г. - * @version 1 - */ -public class WriteFolderStrategy implements GenericWriterStrategy { - - private static final Logger logger = LoggerFactory.getLogger(WriteFolderStrategy.class); - - protected boolean writePreview = true; - - /** - * For internal use. Skips the preview generation when writing the crate. - * - * @return this instance of WriteFolderStrategy - * - * @deprecated May be removed in future versions. Not intended for public use. - */ - @Deprecated(since = "2.1.0", forRemoval = true) - public WriteFolderStrategy disablePreview() { - this.writePreview = false; - return this; - } - - @Override - public void save(Crate crate, String destination) throws IOException { - File file = new File(destination); - FileUtils.forceMkdir(file); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node = objectMapper.readTree(crate.getJsonMetadata()); - String str = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); - InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); - - File json = new File(destination, "ro-crate-metadata.json"); - FileUtils.copyInputStreamToFile(inputStream, json); - inputStream.close(); - // save also the preview files to the crate destination - if (crate.getPreview() != null && this.writePreview) { - crate.getPreview().saveAllToFolder(file); - } - for (var e : crate.getUntrackedFiles()) { - if (e.isDirectory()) { - FileUtils.copyDirectoryToDirectory(e, file); - } else { - FileUtils.copyFileToDirectory(e, file); - } - } - for (DataEntity dataEntity : crate.getAllDataEntities()) { - saveToFile(dataEntity, file); - } - } - - private void saveToFile(DataEntity entity, File file) throws IOException { - if (entity.getPath() != null) { - String id = entity.getId(); - String filename = IdentifierUtils.decode(id).orElse(id); - Path baseFolder = file.toPath().toAbsolutePath().normalize(); - Path destination = baseFolder.resolve(filename).normalize(); - // defence-in-depth: ensure the resolved path remains inside the crate folder - if (!destination.startsWith(baseFolder)) { - logger.warn("Skipping entity '{}': resolved path '{}' escapes destination folder '{}'", id, destination, file); - return; - } - if (entity.getPath().toFile().isDirectory()) { - FileUtils.copyDirectory(entity.getPath().toFile(), destination.toFile()); - } else { - FileUtils.copyFile(entity.getPath().toFile(), destination.toFile()); - } - } - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java deleted file mode 100644 index 003f7973..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java +++ /dev/null @@ -1,33 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; - -import edu.kit.datamanager.ro_crate.Crate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Implementation of the writing strategy to provide a way of writing crates to - * a zip archive. - */ -public class WriteZipStrategy implements - GenericWriterStrategy, - ElnFormatWriter -{ - private static final Logger logger = LoggerFactory.getLogger(WriteZipStrategy.class); - protected ElnFormatWriter delegate = new WriteZipStreamStrategy(); - - @Override - public ElnFormatWriter usingElnStyle() { - this.delegate = this.delegate.withRootSubdirectory(); - return this; - } - - @Override - public void save(Crate crate, String destination) throws IOException { - this.delegate.save(crate, new FileOutputStream(destination)); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java deleted file mode 100644 index 8dfb6adf..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java +++ /dev/null @@ -1,223 +0,0 @@ -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.Crate; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import edu.kit.datamanager.ro_crate.preview.CratePreview; -import edu.kit.datamanager.ro_crate.special.IdentifierUtils; -import edu.kit.datamanager.ro_crate.util.FileSystemUtil; -import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; -import net.lingala.zip4j.io.outputstream.ZipOutputStream; -import net.lingala.zip4j.model.ZipParameters; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of the writing strategy to provide a way of writing crates to - * a zip archive. - */ -public class WriteZipStreamStrategy implements - GenericWriterStrategy, - ElnFormatWriter { - - private static final Logger logger = LoggerFactory.getLogger(WriteZipStreamStrategy.class); - public static final String TMP_DIR = "./.tmp/ro-crate-java/writer-zip-stream-strategy/"; - - /** - * Defines if the zip file will directly contain the crate, - * or if it will contain a subdirectory with the crate. - */ - protected boolean createRootSubdir = false; - - /** - * In streams, we do not have a file name yet (or do not know it), - * so we need to set a default name for the root subdirectory. - */ - protected String rootSubdirName = "content"; - - @Override - public ElnFormatWriter usingElnStyle() { - this.createRootSubdir = true; - return this; - } - - /** - * Sets the name of a root subdirectory in the zip file. - * Implicitly also enables the creation of a root subdirectory. - * If used for ELN files, note the subdirectory name should be the same as the zip - * files name. - * - * @param name the name of the subdirectory - * @return this instance of ReadZipStreamStrategy - */ - public WriteZipStreamStrategy setSubdirectoryName(String name) { - this.rootSubdirName = name; - this.createRootSubdir = true; - return this; - } - - @Override - public void save(Crate crate, OutputStream destination) throws IOException { - String innerFolderName = ""; - if (this.createRootSubdir) { - innerFolderName = FileSystemUtil.filterExtensionsFromFileName( - this.rootSubdirName, - Set.of("ELN", "ZIP")); - innerFolderName = FileSystemUtil.ensureTrailingSlash(innerFolderName); - } - try (ZipOutputStream zipFile = new ZipOutputStream(destination)) { - saveMetadataJson(crate, zipFile, innerFolderName); - saveDataEntities(crate, zipFile, innerFolderName); - savePreview(crate, zipFile, innerFolderName); - } - } - - private void saveDataEntities(Crate crate, ZipOutputStream zipStream, String prefix) throws IOException { - for (DataEntity dataEntity : crate.getAllDataEntities()) { - this.saveToStream(dataEntity, zipStream, prefix); - } - } - - private void saveMetadataJson(Crate crate, ZipOutputStream zipStream, String prefix) throws IOException { - // write the metadata.json file - ZipParameters zipParameters = new ZipParameters(); - zipParameters.setFileNameInZip(prefix + "ro-crate-metadata.json"); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - // we create an JsonNode only to have the file written pretty - JsonNode node = objectMapper.readTree(crate.getJsonMetadata()); - String str = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); - // write the ro-crate-metadata - - byte[] buff = new byte[4096]; - int readLen; - zipStream.putNextEntry(zipParameters); - try (InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))) { - while ((readLen = inputStream.read(buff)) != -1) { - zipStream.write(buff, 0, readLen); - } - } - zipStream.closeEntry(); - } - - private void savePreview(Crate crate, ZipOutputStream zipStream, String prefix) throws IOException { - Optional preview = Optional.ofNullable(crate.getPreview()); - if (preview.isEmpty()) { - return; - } - final String ID = UUID.randomUUID().toString(); - File tmpPreviewFolder = Path.of(TMP_DIR) - .resolve(ID) - .toFile(); - FileUtils.forceMkdir(tmpPreviewFolder); - FileUtils.forceDeleteOnExit(tmpPreviewFolder); - - preview.get().generate(crate, tmpPreviewFolder); - String[] paths = tmpPreviewFolder.list(); - if (paths == null) { - throw new IOException("No preview files found in temporary folder. Preview generation failed."); - } - for (String path : paths) { - File file = tmpPreviewFolder.toPath().resolve(path).toFile(); - if (file.isDirectory()) { - ZipStreamUtil.addFolderToZipStream( - zipStream, - file, - prefix + path); - } else { - ZipStreamUtil.addFileToZipStream( - zipStream, - file, - prefix + path); - } - } - try { - FileUtils.forceDelete(tmpPreviewFolder); - } catch (IOException e) { - logger.error("Could not delete temporary preview folder: {}", tmpPreviewFolder); - } - } - - private void saveToStream(DataEntity entity, ZipOutputStream zipStream, String prefix) throws IOException { - if (entity == null) { - return; - } - - boolean isDirectory = entity.getPath().toFile().isDirectory(); - String id = entity.getId(); - String filename = IdentifierUtils.decode(id).orElse(id); - String safeName = sanitizeZipEntryName(filename); - if (safeName.isEmpty()) { - logger.warn("Skipping entity '{}': decoded name resolves outside the crate root", id); - return; - } - String entryName = prefix + safeName; - if (isDirectory) { - ZipStreamUtil.addFolderToZipStream( - zipStream, - entity.getPath().toFile(), - entryName); - } else { - ZipStreamUtil.addFileToZipStream( - zipStream, - entity.getPath().toFile(), - entryName); - } - } - - /** - * Normalizes a decoded entry name into a safe relative path, consistent - * with the containment check in {@link WriteFolderStrategy#saveToFile}. - *

- * Absolute paths (leading {@code /}) and Windows drive-qualified paths - * (e.g. {@code C:/}) are rejected. Internal {@code .} and {@code ..} - * segments are resolved; if the result escapes the virtual crate root - * the name is rejected. - * - * @param name the raw decoded entry name - * @return a canonical relative entry name, or an empty string if the name - * is absolute, drive-qualified, or escapes the crate root - */ - private static String sanitizeZipEntryName(String name) { - // zip entries always use forward slashes as separators - String normalized = name.replace('\\', '/'); - - // Reject absolute paths and Windows drive-qualified paths (e.g. C:/) - if (normalized.startsWith("/") - || (normalized.length() >= 2 && normalized.charAt(1) == ':' - && Character.isLetter(normalized.charAt(0)))) { - return ""; - } - - // Normalize "." and ".." segments, rejecting paths that escape the root - String[] segments = normalized.split("/"); - List stack = new ArrayList<>(); - for (String segment : segments) { - if (segment.isEmpty() || segment.equals(".")) { - continue; - } - if (segment.equals("..")) { - if (stack.isEmpty()) { - return ""; - } - stack.remove(stack.size() - 1); - } else { - stack.add(segment); - } - } - return String.join("/", stack); - } -} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java deleted file mode 100644 index d1a41d66..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java +++ /dev/null @@ -1,42 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; - -import java.io.OutputStream; - -/** - * Utility class for creating instances of different crate writers. - * This class is not meant to be instantiated. - */ -public class Writers { - - /** - * Prevents instantiation of this utility class. - */ - private Writers() {} - - /** - * Creates a new instance of a crate writer that writes to a folder. - * - * @return a new instance of {@link CrateWriter} for writing to a folder - */ - public static CrateWriter newFolderWriter() { - return new CrateWriter<>(new WriteFolderStrategy()); - } - - /** - * Creates a new instance of a crate writer that writes to a zip stream. - * - * @return a new instance of {@link CrateWriter} for writing to a zip stream - */ - public static CrateWriter newZipStreamWriter() { - return new CrateWriter<>(new WriteZipStreamStrategy()); - } - - /** - * Creates a new instance of a crate writer that writes to a zip file. - * - * @return a new instance of {@link CrateWriter} for writing to a zip file - */ - public static CrateWriter newZipPathWriter() { - return new CrateWriter<>(new WriteZipStrategy()); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java b/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java deleted file mode 100644 index 24a57a07..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java +++ /dev/null @@ -1,178 +0,0 @@ -package edu.kit.datamanager.ro_crate; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import com.fasterxml.jackson.databind.SerializationFeature; -import edu.kit.datamanager.ro_crate.entities.AbstractEntity; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; - -import org.apache.commons.io.FileUtils; -import io.json.compare.JSONCompare; -import io.json.compare.JsonComparator; -import org.opentest4j.AssertionFailedError; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class HelpFunctions { - - public static void compareEntityWithFile(AbstractEntity entity, String string) throws IOException { - InputStream inputStream - = HelpFunctions.class.getResourceAsStream(string); - JsonNode expectedJson = MyObjectMapper.getMapper().readTree(inputStream); - JsonNode node = MyObjectMapper.getMapper().convertValue(entity, JsonNode.class); - //compare the size of the expected and actual node. Both nodes should have the same number of properties. - assertEquals(expectedJson.size(), node.size()); - compare(expectedJson, node, true); - } - - public static void compare(JsonNode node1, JsonNode node2, Boolean equals) { - var comparator = new JsonComparator() { - public boolean compareValues(Object expected, Object actual) { - return expected.equals(actual); - } - - public boolean compareFields(String expected, String actual) { - return expected.equals(actual); - } - }; - - if (equals) { - JSONCompare.assertMatches(node1, node2, comparator); - } else { - JSONCompare.assertNotMatches(node1, node2, comparator); - } - } - - public static void compareTwoMetadataJsonNotEqual(Crate crate1, Crate crate2) throws JsonProcessingException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node1 = objectMapper.readTree(crate1.getJsonMetadata()); - JsonNode node2 = objectMapper.readTree(crate2.getJsonMetadata()); - compare(node1, node2, false); - } - - public static void compareTwoMetadataJsonNotEqual(Crate crate1, String jsonFileString) throws IOException { - InputStream inputStream = HelpFunctions.class.getResourceAsStream( - jsonFileString); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node1 = objectMapper.readTree(crate1.getJsonMetadata()); - JsonNode node2 = JsonUtilFunctions.unwrapSingleArray(objectMapper.readTree(inputStream)); - compare(node1, node2, false); - } - - public static void compareTwoCrateJson(Crate crate1, Crate crate2) throws JsonProcessingException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node1 = objectMapper.readTree(crate1.getJsonMetadata()); - JsonNode node2 = objectMapper.readTree(crate2.getJsonMetadata()); - compare(node1, node2, true); - } - - public static void prettyPrintJsonString(String minimalJsonMetadata) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode jsonNode = objectMapper.readTree(minimalJsonMetadata); - // Enable pretty printing - String prettyJson = objectMapper - .enable(SerializationFeature.INDENT_OUTPUT) - .writeValueAsString(jsonNode); - // Print the pretty JSON - System.out.println(prettyJson); - } catch (JsonProcessingException e) { - throw new AssertionFailedError("Not able to process string as JSON!", e); - } - } - - public static void printAndAssertEquals(RoCrate crate, String pathToResource) { - // So you get something to see - prettyPrintJsonString(crate.getJsonMetadata()); - // Compare with the example from the specification - try { - HelpFunctions.compareCrateJsonToFileInResources(crate, pathToResource); - } catch (IOException e) { - throw new AssertionFailedError("Missing resources file!", e); - } - } - - public static void compareCrateJsonToFileInResources(File file1, File file2) throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node1 = JsonUtilFunctions.unwrapSingleArray(objectMapper.readTree(file1)); - JsonNode node2 = JsonUtilFunctions.unwrapSingleArray(objectMapper.readTree(file2)); - compare(node1, node2, true); - } - - /** - * Compares the JSON metadata of a Crate object with a JSON file in the resources directory. - * - * @param crate1 The Crate object to compare. - * @param jsonFileString The path to the JSON file in the resources directory. - * @throws IOException If an error occurs while reading the JSON file. - */ - public static void compareCrateJsonToFileInResources(Crate crate1, String jsonFileString) throws IOException { - InputStream inputStream = HelpFunctions.class.getResourceAsStream( - jsonFileString); - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode node1 = objectMapper.readTree(crate1.getJsonMetadata()); - JsonNode node2 = JsonUtilFunctions.unwrapSingleArray(objectMapper.readTree(inputStream)); - compare(node1, node2, true); - } - - public static boolean compareTwoDir(File dir1, File dir2) throws IOException { - // compare the content of the two directories - Map compareWithMe = FileUtils.listFiles(dir1, null, true) - .stream() - .collect(Collectors.toMap(java.io.File::getName, Function.identity())); - - Map testMe = FileUtils.listFiles(dir2, null, true) - .stream() - .collect(Collectors.toMap(java.io.File::getName, Function.identity())); - - - if (compareWithMe.size() != testMe.size()) { - return false; - } - for (String filename : testMe.keySet()) { - // we do that because the ro-crate-metadata.json can be differently formatted, - // or the order of the entities may be different - // the same holds for the html file - if (filename.equals("ro-crate-preview.html") || filename.equals("ro-crate-metadata.json")) { - if (!compareWithMe.containsKey(filename)) { - return false; - } - } else if (!FileUtils.contentEqualsIgnoreEOL(testMe.get(filename), compareWithMe.get(filename), null)) { - return false; - } - } - return true; - } - - /** - * Prints the file tree of the given directory for debugging and understanding - * a test more quickly. - * - * @param directoryToPrint the directory to print - * @throws IOException if an error occurs while printing the file tree - */ - @SuppressWarnings("resource") - public static void printFileTree(Path directoryToPrint) throws IOException { - // Print all files recursively in a tree structure for debugging - System.out.printf("Files in %s:%n", directoryToPrint.getFileName().toString()); - Files.walk(directoryToPrint) - .forEach(path -> { - if (!path.toAbsolutePath().equals(directoryToPrint.toAbsolutePath())) { - int depth = path.relativize(directoryToPrint).getNameCount(); - String prefix = " ".repeat(depth); - System.out.printf("%s%s%s%n", prefix, "└── ", path.getFileName()); - } - }); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java deleted file mode 100644 index 270127af..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java +++ /dev/null @@ -1,82 +0,0 @@ -package edu.kit.datamanager.ro_crate.crate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; - -import edu.kit.datamanager.ro_crate.reader.Readers; -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.JsonNode; - -import edu.kit.datamanager.ro_crate.Crate; -import edu.kit.datamanager.ro_crate.RoCrate; -import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; -import edu.kit.datamanager.ro_crate.special.CrateVersion; -import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; -import edu.kit.datamanager.ro_crate.validation.Validator; - -class BuilderSpec12Test { - private URI profile1; - private URI profile2; - - @Test - void testAppendConformsTo() throws URISyntaxException { - Crate crate = new RoCrate.BuilderWithDraftFeatures() - .alsoConformsTo(new URI("https://w3id.org/ro/wfrun/process/0.1")) - .alsoConformsTo(new URI("https://example.com/myprofile/1.0")) - .build(); - JsonNode conformsTo = crate.getJsonDescriptor().getProperty("conformsTo"); - assertTrue(conformsTo.isArray()); - // one version and two profiles - assertEquals(1 + 2, conformsTo.size()); - } - - @Test - void testModificationOfDraftCrate() throws URISyntaxException, IOException { - String path = this.getClass().getResource("/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array").getPath(); - RoCrate crate = Readers.newFolderReader().readCrate(path); - Collection existingProfiles = crate.getProfiles(); - profile1 = new URI("https://example.com/myprofile/1.0"); - profile2 = new URI("https://example.com/myprofile/2.0"); - // the loaded crate has at least one profile - assertFalse(existingProfiles.isEmpty()); - // and the ones we will add later are not part of it - assertFalse(existingProfiles.contains(profile1.toString())); - assertFalse(existingProfiles.contains(profile2.toString())); - - // add profiles - RoCrate modifiedCrate = new RoCrate.BuilderWithDraftFeatures(crate) - .alsoConformsTo(profile1) - .alsoConformsTo(profile2) - .addContextualEntity(new ContextualEntity.ContextualEntityBuilder() - .addType("CreativeWork") - .build()) - .build(); - - // sanity checks - Validator defaultValidation = new Validator(new JsonSchemaValidation()); - assertTrue(defaultValidation.validate(modifiedCrate)); - assertEquals(CrateVersion.LATEST_UNSTABLE, crate.getVersion().get()); - assertEquals(CrateVersion.LATEST_UNSTABLE, modifiedCrate.getVersion().get()); - - // number of profiles increased by 2 - Collection newProfileState = modifiedCrate.getProfiles(); - assertEquals(existingProfiles.size() + 2, newProfileState.size()); - // new profiles are present - assertTrue(newProfileState.contains(profile1.toString())); - assertTrue(newProfileState.contains(profile2.toString())); - // old profiles are present - assertEquals( - 0, - existingProfiles.stream() - .filter(txt -> !newProfileState.contains(txt)) - .count() - ); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java deleted file mode 100644 index bc959c29..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java +++ /dev/null @@ -1,178 +0,0 @@ -package edu.kit.datamanager.ro_crate.crate; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import edu.kit.datamanager.ro_crate.HelpFunctions; -import edu.kit.datamanager.ro_crate.RoCrate; -import edu.kit.datamanager.ro_crate.entities.contextual.PersonEntity; -import edu.kit.datamanager.ro_crate.entities.contextual.PlaceEntity; -import edu.kit.datamanager.ro_crate.entities.data.FileEntity; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; - -public class TestRemoveAddEntity { - @Test - void testAddRemoveEntity() throws IOException { - RoCrate roCrate = new RoCrate.RoCrateBuilder("minimal", "minimal RO_crate", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addDataEntity( - new FileEntity.FileEntityBuilder() - .setId("survey-responses-2019.csv") - .addProperty("name", "Survey responses") - .addProperty("contentSize", "26452") - .addProperty("encodingFormat", "text/csv") - .build() - ) - .build(); - HelpFunctions.compareCrateJsonToFileInResources(roCrate, "/json/crate/onlyOneFile.json"); - - assertEquals(0, roCrate.getAllContextualEntities().size()); - assertEquals(1, roCrate.getAllDataEntities().size()); - - // remove entity and check if equals to the basic crate - roCrate.deleteEntityById("survey-responses-2019.csv"); - assertEquals(0, roCrate.getAllDataEntities().size()); - HelpFunctions.compareCrateJsonToFileInResources(roCrate, "/json/crate/simple.json"); - } - - @Test - void withTwoFiles() throws IOException { - PlaceEntity place = new PlaceEntity.PlaceEntityBuilder() - .setId("http://sws.geonames.org/8152662/") - .addProperty("name", "Catalina Park") - .build(); - - PersonEntity person = new PersonEntity.PersonEntityBuilder() - .setId("#alice") - .addProperty("name", "Alice") - .addProperty("description", "One of hopefully many Contextual Entities") - .build(); - - FileEntity file = new FileEntity.FileEntityBuilder() - .setId("data1.txt") - .addProperty("description", "One of hopefully many Data Entities") - .setContentLocation(place.getId()) - .addAuthor(person.getId()) - .build(); - - RoCrate roCrate = new RoCrate.RoCrateBuilder("Example RO-Crate", - "The RO-Crate Root Data Entity", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addContextualEntity(place) - .addContextualEntity(person) - .addDataEntity(file) - .addDataEntity( - new FileEntity.FileEntityBuilder().setId("data2.txt").build() - ) - .build(); - - HelpFunctions.compareCrateJsonToFileInResources(roCrate, "/json/crate/twoFiles.json"); - - assertEquals(2, roCrate.getAllDataEntities().size()); - assertEquals(2, roCrate.getAllContextualEntities().size()); - - roCrate.deleteEntityById("data1.txt"); - assertEquals(1, roCrate.getAllDataEntities().size()); - - roCrate.deleteEntityById("data2.txt"); - assertEquals(0, roCrate.getAllDataEntities().size()); - - roCrate.deleteEntityById("#alice"); - assertEquals(1, roCrate.getAllContextualEntities().size()); - - roCrate.deleteEntityById("http://sws.geonames.org/8152662/"); - assertEquals(0, roCrate.getAllContextualEntities().size()); - - RoCrate roCrate2 = new RoCrate.RoCrateBuilder("Example RO-Crate", - "The RO-Crate Root Data Entity", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/").build(); - HelpFunctions.compareTwoCrateJson(roCrate, roCrate2); - } - - @Test - void removeOtherOccur() throws JsonProcessingException { - PlaceEntity place = new PlaceEntity.PlaceEntityBuilder() - .setId("http://sws.geonames.org/8152662/") - .addProperty("name", "Catalina Park") - .build(); - - PersonEntity person = new PersonEntity.PersonEntityBuilder() - .setId("#alice") - .addProperty("name", "Alice") - .addProperty("description", "One of hopefully many Contextual Entities") - .build(); - - FileEntity file = new FileEntity.FileEntityBuilder() - .setId("data1.txt") - .addProperty("description", "One of hopefully many Data Entities") - .setContentLocation(place.getId()) - .addAuthor(person.getId()) - .addAuthor(person.getId()) - .build(); - - RoCrate roCrate = new RoCrate.RoCrateBuilder("Example RO-Crate", - "The RO-Crate Root Data Entity", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addContextualEntity(place) - .addContextualEntity(person) - .addDataEntity(file) - .build(); - - assertEquals(1, roCrate.getAllDataEntities().size()); - assertEquals(2, roCrate.getAllContextualEntities().size()); - - roCrate.deleteEntityById(person.getId()); - assertEquals(1, roCrate.getAllContextualEntities().size()); - - roCrate.deleteEntityById(place.getId()); - assertEquals(0, roCrate.getAllContextualEntities().size()); - assertEquals(1, roCrate.getAllDataEntities().size()); - - FileEntity file2 = new FileEntity.FileEntityBuilder() - .setId("data1.txt") - .addProperty("description", "One of hopefully many Data Entities") - .build(); - RoCrate second2 = new RoCrate.RoCrateBuilder("Example RO-Crate", "The RO-Crate Root Data Entity", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addDataEntity(file2) - .build(); - - HelpFunctions.compareTwoCrateJson(roCrate, second2); - } - - @Test - void testAddEntitiesWithSameId() { - - PersonEntity person = new PersonEntity.PersonEntityBuilder() - .setId("person") - .addProperty("name", "Alice") - .addProperty("description", "One of hopefully many Contextual Entities") - .build(); - - RoCrate roCrate = new RoCrate.RoCrateBuilder("Example RO-Crate", - "The RO-Crate Root Data Entity", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addContextualEntity(person) - .build(); - - RoCrate roCrateNew = new RoCrate.RoCrateBuilder("Example RO-Crate", - "The RO-Crate Root Data Entity", "2023", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .addContextualEntity(person) - .build(); - - assertEquals(1, roCrate.getAllContextualEntities().size()); - assertEquals(1, roCrateNew.getAllContextualEntities().size()); - - PersonEntity personNew = new PersonEntity.PersonEntityBuilder() - .setId("person") - .addProperty("name", "Peter Sefton") - .addProperty("email", "peter.sefton@uts.edu.au") - .build(); - - roCrateNew.addContextualEntity(personNew); - - assertEquals(1, roCrate.getAllContextualEntities().size()); - assertEquals(1, roCrateNew.getAllContextualEntities().size()); - - assertEquals(roCrate.getContextualEntityById("person"), person); - assertEquals(roCrateNew.getContextualEntityById("person"), personNew); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java deleted file mode 100644 index 678642ba..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; - -class AbstractEntityTest { - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" ", "\t", "\n"}) - void mergeIdIntoValue_withInvalidId_returnsEmpty(String invalidId) { - Optional result = AbstractEntity.mergeIdIntoValue(invalidId, null); - assertTrue(result.isEmpty(), "Should return empty Optional for invalid ID"); - - Optional result2 = AbstractEntity.mergeIdIntoValue( - invalidId, - MyObjectMapper.getMapper().createObjectNode() - ); - assertTrue(result2.isEmpty(), "Should return empty Optional for invalid ID"); - } - - @Test - void mergeIdIntoValue_withNullCurrentValue_returnsIdObject() { - String id = "test-id"; - Optional result = AbstractEntity.mergeIdIntoValue(id, null); - - // Should return a value for valid ID - JsonNode node = result.orElseThrow(); - assertTrue(node.isObject(), "Should return an object"); - assertEquals(id, node.get("@id").asText(), "Should contain the ID"); - } - - @Test - void mergeIdIntoValue_withExistingIdAsString_returnsEmpty() { - String id = "test-id"; - JsonNode currentValue = MyObjectMapper.getMapper().valueToTree(id); - - Optional result = AbstractEntity.mergeIdIntoValue(id, currentValue); - assertTrue(result.isEmpty(), "Should return empty when ID already exists as string"); - } - - @Test - void mergeIdIntoValue_withExistingIdObject_returnsEmpty() { - String id = "test-id"; - ObjectNode currentValue = MyObjectMapper.getMapper().createObjectNode(); - currentValue.put("@id", id); - - Optional result = AbstractEntity.mergeIdIntoValue(id, currentValue); - assertTrue(result.isEmpty(), "Should return empty when ID already exists as object"); - } - - @Test - void mergeIdIntoValue_withNonArrayValue_createsArray() { - String id = "new-id"; - String existingId = "existing-id"; - ObjectNode existingValue = MyObjectMapper.getMapper().createObjectNode(); - existingValue.put("@id", existingId); - - Optional result = AbstractEntity.mergeIdIntoValue(id, existingValue); - - assertTrue(result.isPresent(), "Should return a value"); - JsonNode node = result.get(); - assertTrue(node.isArray(), "Should be converted to array"); - assertEquals(2, node.size(), "Should contain both values"); - assertEquals(existingId, node.get(0).get("@id").asText(), "Should contain existing ID"); - assertEquals(id, node.get(1).get("@id").asText(), "Should contain new ID"); - } - - @Test - void mergeIdIntoValue_withExistingArray_addsToArray() { - String id = "new-id"; - String existingId = "existing-id"; - - ArrayNode currentValue = MyObjectMapper.getMapper().createArrayNode(); - ObjectNode existingIdObj = MyObjectMapper.getMapper().createObjectNode(); - existingIdObj.put("@id", existingId); - currentValue.add(existingIdObj); - - Optional result = AbstractEntity.mergeIdIntoValue(id, currentValue); - - assertTrue(result.isPresent(), "Should return a value"); - JsonNode node = result.get(); - assertTrue(node.isArray(), "Should remain an array"); - assertEquals(2, node.size(), "Should contain both values"); - assertEquals(existingId, node.get(0).get("@id").asText(), "Should contain existing ID"); - assertEquals(id, node.get(1).get("@id").asText(), "Should contain new ID"); - } - - @Test - void mergeIdIntoValue_withExistingArrayContainingId_returnsEmpty() { - String id = "test-id"; - ArrayNode currentValue = MyObjectMapper.getMapper().createArrayNode(); - ObjectNode existingIdObj = MyObjectMapper.getMapper().createObjectNode(); - existingIdObj.put("@id", id); - currentValue.add(existingIdObj); - - Optional result = AbstractEntity.mergeIdIntoValue(id, currentValue); - assertTrue(result.isEmpty(), "Should return empty when ID already exists in array"); - } -} - diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java deleted file mode 100644 index 7bc868f3..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.contextual; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; -import edu.kit.datamanager.ro_crate.HelpFunctions; - -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * @author Nikola Tzotchev on 5.2.2022 г. - * @version 1 - */ -public class ContextualEntityTest { - - /** - * This is exactly the same test as the PlaceEntityTest it shows again (similarly to the - * DataEntityTest) that we can also just use this class for the creation of our Contextual - * Entities - */ - @Test - void testSerialization() throws IOException { - // this does not make any difference for our testcase it just shows how the GeoCoordinates entity will look - ContextualEntity geo = new ContextualEntity.ContextualEntityBuilder() - .setId("#b4168a98-8534-4c6d-a568-64a55157b656") - .addType("GeoCoordinates") - .addProperty("latitude", "-33.7152") - .addProperty("longitude", "150.30119") - .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") - .build(); - - ContextualEntity place = new ContextualEntity.ContextualEntityBuilder() - .addType("Place") - .setId("https://sws.geonames.org/8152662/") - .addProperty("description", - "Catalina Park is a disused motor racing venue, located at Katoomba ...") - .addProperty("identifier", "https://sws.geonames.org/8152662/") - .addProperty("uri", "https://www.geonames.org/8152662/catalina-park.html") - .addProperty("name", "Catalina Park") - // here we can also do .setGeo(geo) - .addIdProperty("geo", geo) - .build(); - - assertTrue(place.getLinkedTo().contains(geo.getId())); - HelpFunctions.compareEntityWithFile(place, "/json/entities/contextual/place.json"); - } - - @Test - void testAddAllValidCase() throws JsonProcessingException { - ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() - .setId("#b4168a98-8534-4c6d-a568-64a55157b656") - .addType("GeoCoordinates") - .addProperty("latitude", "-33.7152") - .addProperty("longitude", "150.30119") - .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") - .build(); - - String allProperties = """ - { - "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", - "@type": "GeoCoordinates", - "latitude": "-33.7152", - "longitude": "150.30119", - "name": "Latitude: -33.7152 Longitude: 150.30119" - } - """; - - ObjectNode properties = MyObjectMapper.getMapper() - .readValue(allProperties, ObjectNode.class); - ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() - .setAllIfValid(properties) - .build(); - assertEquals(second.getProperties(), first.getProperties()); - } - - @Test - void testAddAllInvalidCase() throws JsonProcessingException { - ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() - .setId("#b4168a98-8534-4c6d-a568-64a55157b656") - .addType("GeoCoordinates") - .addProperty("latitude", "-33.7152") - .addProperty("longitude", "150.30119") - .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") - .build(); - - String allProperties = """ - { - "wrong property": {"any": "value"}, - "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", - "@type": "GeoCoordinates", - "latitude": "-33.7152", - "longitude": "150.30119", - "name": "Latitude: -33.7152 Longitude: 150.30119" - } - """; - - ObjectNode properties = MyObjectMapper.getMapper() - .readValue(allProperties, ObjectNode.class); - ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .setAllIfValid(properties) - .build(); - assertNotEquals(second.getProperties(), first.getProperties()); - ObjectNode empty = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .build() - .getProperties(); - assertEquals(empty, second.getProperties()); - } - - @Test - void testAddAllUnsafeDoesInvalidCase() throws JsonProcessingException { - ContextualEntity first = new ContextualEntity.ContextualEntityBuilder() - .setId("#b4168a98-8534-4c6d-a568-64a55157b656") - .addType("GeoCoordinates") - .addProperty("latitude", "-33.7152") - .addProperty("longitude", "150.30119") - .addProperty("name", "Latitude: -33.7152 Longitude: 150.30119") - .build(); - - String allProperties = """ - { - "wrong property": {"any": "value"}, - "@id": "#b4168a98-8534-4c6d-a568-64a55157b656", - "@type": "GeoCoordinates", - "latitude": "-33.7152", - "longitude": "150.30119", - "name": "Latitude: -33.7152 Longitude: 150.30119" - } - """; - - ObjectNode properties = MyObjectMapper.getMapper() - .readValue(allProperties, ObjectNode.class); - ContextualEntity second = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .setAllUnsafe(properties) - .build(); - assertNotEquals(second.getProperties(), first.getProperties()); - ObjectNode empty = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .build() - .getProperties(); - assertNotEquals(empty, second.getProperties()); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java deleted file mode 100644 index f551c891..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.data; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.IOException; -import java.net.URL; - -import edu.kit.datamanager.ro_crate.HelpFunctions; -import edu.kit.datamanager.ro_crate.entities.data.DataEntity.DataEntityBuilder; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import java.net.URI; - -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author Nikola Tzotchev on 4.2.2022 г. - * @version 1 - */ -class DataEntityTest { - - @Test - void testSerialization() throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - - ObjectNode bigJson = objectMapper.createObjectNode(); - bigJson.put("onefield", "dalkfa"); - ObjectNode address = objectMapper.createObjectNode(); - address.put("street", "x"); - address.put("city", "other"); - address.put("state", "this"); - address.put("zipCode", 66439); - bigJson.set("nestedObject", address); - - DataEntity file = new DataEntityBuilder() - .addType("File") - .setId("https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf") - .addProperty("name", "RO-Crate specification") - .addProperty("encodingFormat", "application/pdf") - .addProperty("url", "https://zenodo.org/record/3541888") - // this should not be included since it does not follow the json flatten form of the RO-Crate - .addProperty("test", bigJson) - .build(); - assertNull(file.getProperty("test")); - HelpFunctions.compareEntityWithFile(file, "/json/entities/data/fileEntity.json"); - } - - @Test - void testSerializationOutside() throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode json = objectMapper.createObjectNode(); - json.put("name", "RO-Crate specification"); - json.put("encodingFormat", "application/pdf"); - json.put("url", "https://zenodo.org/record/3541888"); - json.put("@id", "https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf"); - json.put("@type", "File"); - - DataEntity data = new DataEntity.DataEntityBuilder().build(); - data.setProperties(json); - HelpFunctions.compareEntityWithFile(data, "/json/entities/data/fileEntity.json"); - - ObjectNode address = objectMapper.createObjectNode(); - address.put("street", "x"); - address.put("city", "other"); - address.put("state", "this"); - address.put("zipCode", 66439); - json.set("nestedObject", address); - // this should not be included into the entity since it does not follow the basic RO-Crate structure - data.setProperties(json); - assertNotEquals("", data.getId()); - HelpFunctions.compareEntityWithFile(data, "/json/entities/data/fileEntity.json"); - } - - @Test - void testUriCheck() { - //this entity id is a valid URL so there should not be any console information - DataEntity dataEntity = new DataEntity.DataEntityBuilder() - .addType("File") - .setLocation(URI.create("https://www.example.com/19")) - .build(); - - assertNotNull(dataEntity); - - Exception exception = assertThrows( - IllegalArgumentException.class, - () -> - new DataEntity.DataEntityBuilder() - .addType("File") - .setLocationWithExceptions(URI.create("zzz://wrong/url")) - .build() - ); - - assertEquals("This Data Entity remote ID does not resolve to a valid URL.", exception.getMessage()); - - assertNotNull(dataEntity); - - } - - @Test - void testPathCheck() throws IOException { - FileEntity dataEntity = new FileEntity.FileEntityBuilder() - .setLocationWithExceptions(Paths.get("example.json")) - .setId("example.json") - .addProperty("name", "RO-Crate specification") - .setEncodingFormat("application/json") - .build(); - assertEquals("example.json", dataEntity.getId()); - HelpFunctions.compareEntityWithFile(dataEntity, "/json/entities/data/localFile.json"); - - String fileName = "cp7glop.ai"; - Path filePath = Paths.get(fileName); - - dataEntity = new FileEntity.FileEntityBuilder() - .setLocationWithExceptions(filePath) - .setId(fileName) - .addProperty("name", "Diagram showing trend to increase") - .setEncodingFormat("application/pdf") - .build(); - assertEquals(fileName, dataEntity.getId()); - assertEquals(dataEntity.getPath(), filePath); - assertEquals("application/pdf", dataEntity.getProperty("encodingFormat").asText()); - } - - @Test - void testEncodedUrl() throws URISyntaxException { - //this entity id is a valid URL so there should not be any console information - DataEntity dataEntity = new DataEntity.DataEntityBuilder() - .addType("File") - .setLocation(URI.create("https://github.com/kit-data-manager/ro-crate-java/issues/5")) - .build(); - - assertEquals("https://github.com/kit-data-manager/ro-crate-java/issues/5", dataEntity.getId()); - - URL url - = HelpFunctions.class.getResource("/json/crate/simple2.json"); - assert url != null; - dataEntity = new DataEntity.DataEntityBuilder() - .addType("File") - .setLocationWithExceptions(Paths.get(url.toURI())) - .setId("simple2.json") - .build(); - - assertNotNull(dataEntity); - - dataEntity = new DataEntity.DataEntityBuilder() - .addType("File") - .setLocationWithExceptions(Paths.get("/json/crate/Results%20and%20Diagrams/almost-50%25.json")) - .setId("almost-50%25.json") - .build(); - - assertTrue(dataEntity.getTypes().contains("File")); - assertEquals("almost-50%25.json", dataEntity.getId()); - - // even if the Path is not correctly encoded, the data entity will be added with an encoded Path. - dataEntity = new DataEntity.DataEntityBuilder() - .addType("File") - .setLocationWithExceptions(Paths.get("/json/crate/Results and Diagrams/almost-50%.json")) - .setId("almost-50%.json") - .build(); - assertEquals("almost-50%25.json", dataEntity.getId()); - - } - - @Test - void testRemoveProperty() { - FileEntity dataEntity = new FileEntity.FileEntityBuilder() - .addProperty("name", "RO-Crate specification") - .setEncodingFormat("application/json") - .addProperty("url", "https://zenodo.org/record/3541888") - .addProperty("@id", "https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf") - .build(); - - dataEntity.removeProperty("url"); - assertNull(dataEntity.getProperty("url")); - assertEquals(4, dataEntity.getProperties().size()); - - List keyList = Arrays.asList("encodingFormat", "name"); - dataEntity.removeProperties(keyList); - assertEquals(2, dataEntity.getProperties().size()); - } - - @Test - void testMixedArrayProperties() { - ObjectMapper mapper = new ObjectMapper(); - - ArrayNode propertyValue = mapper.createArrayNode(); - propertyValue - .add(1) - .add("2") - .add(3.14) - .add(true) - .add((JsonNode) null) - .add(mapper.nullNode()); - String propertyName = "mixedArray"; - - DataEntity entity = new DataEntityBuilder() - .addProperty(propertyName, propertyValue) - .build(); - assertEquals(propertyValue, entity.getProperty(propertyName)); - } - - @ParameterizedTest - @MethodSource("provideInvalidPropertyValues") - void testNestedArrayProperties(ArrayNode propertyValue) { - String propertyName = "invalidArray"; - DataEntity entity = new DataEntityBuilder() - .addProperty(propertyName, propertyValue) - .build(); - assertNull(entity.getProperty(propertyName)); - } - - private static Stream provideInvalidPropertyValues() { - ObjectMapper mapper = new ObjectMapper(); - ArrayNode withSubArray = mapper.createArrayNode().add(1).add("2"); - withSubArray.addArray(); - ArrayNode withSubObject = mapper.createArrayNode().add(1).add("2"); - withSubObject.addObject(); - - return Stream.of(withSubArray, withSubObject); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java deleted file mode 100644 index 5ae4142d..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.data; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.IOException; - -import edu.kit.datamanager.ro_crate.HelpFunctions; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import java.net.URI; -import java.nio.file.Paths; -import java.util.Objects; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * @author Nikola Tzotchev on 5.2.2022 г. - * @version 1 - */ -public class DataSetEntityTest { - - @Test - void testImpossibleRootId() { - DataSetEntity e = new DataSetEntity.DataSetBuilder() - .setId("./") - .addProperty("not_root", true) - .build(); - assertNotNull(e.getId()); - assertFalse(e.getId().isBlank()); - assertNotEquals(RootDataEntity.ID, e.getId()); - } - - @Test - void testSimpleDirDeserialization() throws IOException { - - String id = "lots_of_little_files/"; - DataSetEntity dir = new DataSetEntity.DataSetBuilder() - // this does not change anything, but it shows that the entity contains a physical file - .setLocationWithExceptions(Paths.get("invalid_file")) - .setId(id) - .addProperty("name", "Too many files") - .addProperty("description", - "This directory contains many small files, that we're not going to describe in detail.") - .build(); - - assertEquals(id, dir.getId()); - HelpFunctions.compareEntityWithFile(dir, "/json/entities/data/directory.json"); - } - - /** - * This is a test with a directory that is located on the web. It is - * recommended that such a dir list all of its files in the hasPart property - * https://www.researchobject.org/ro-crate/1.1/data-entities.html#directories-on-the-web-dataset-distributions - */ - @Test - void testDirWithHasPartDeserialization() throws IOException { - - String id = "second_content"; - DataEntity second_content = new DataEntity.DataEntityBuilder() - .setLocationWithExceptions(Paths.get("does_not_matter")) - .setId(id) - .addProperty("description", "This entity just describes one of the contents in the Dir") - .build(); - - // we can add to hasPart using directly the id, or passing the entity to it - DataSetEntity dir = new DataSetEntity.DataSetBuilder() - .setLocation(URI.create("https://www.example.com/urltodir")) - .addProperty("name", "Directory that is located on the web") - .addToHasPart("first_content") - .addToHasPart(second_content) - .build(); - - assertTrue(dir.hasPart(id)); - HelpFunctions.compareEntityWithFile(dir, "/json/entities/data/directoryWeb.json"); - } - - @Test - void testWebDir() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode propertyValue = objectMapper.createObjectNode(); - propertyValue.put("@id", "http://example.com/downloads/2020/lots_of_little_files.zip"); - - DataSetEntity dir = new DataSetEntity.DataSetBuilder() - .setLocationWithExceptions(Paths.get("lots_of_little_files/")) - .setId("lots_of_little_files/") - .addProperty("name", "Too many files") - .addProperty("description", "This directory contains many small files, that we're not going to describe in detail.") - .addProperty("distribution", propertyValue) - .build(); - - assertEquals(dir.getProperty("distribution"), propertyValue); - - DataEntity webDir = new DataEntity.DataEntityBuilder() - .setLocation(URI.create("http://example.com/downloads/2020/lots_of_little_files.zip")) - .addType("DataDownload") - .addProperty("encodingFormat", "application/zip") - .addProperty("contentSize", "82818928") - .build(); - - assertNotNull(webDir); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java deleted file mode 100644 index 76033f70..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package edu.kit.datamanager.ro_crate.entities.data; - -import com.fasterxml.jackson.databind.ObjectMapper; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import edu.kit.datamanager.ro_crate.HelpFunctions; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import java.nio.file.Paths; - -import org.junit.jupiter.api.Test; - -/** - * @author Nikola Tzotchev on 5.2.2022 г. - * @version 1 - */ -public class RootDataEntityTest { - - @Test - void testSerialization() throws IOException { - String id1 = "file1_id"; - FileEntity file1 = new FileEntity.FileEntityBuilder() - .setLocationWithExceptions(Paths.get("does_not_matter")) - .setId(id1) - .build(); - - String id2 = "file2_id"; - FileEntity file2 = new FileEntity.FileEntityBuilder() - .addIdProperty("name", "dsklfajs") - .addIdProperty("name", "11111111") - .setLocationWithExceptions(Paths.get("does_not_matter")) - .setId(id2) - .build(); - - RootDataEntity rootDataEntity = new RootDataEntity.RootDataEntityBuilder() - .addProperty("identifier", "https://doi.org/10.4225/59/59672c09f4a4b") - .addProperty("datePublished", "2000-02-01T00:00:00Z") - .addProperty("name", - "Data files associated with the manuscript:Effects of facilitated family case conferencing for ...") - .addProperty("description", - "Palliative care planning for nursing home residents with advanced dementia ...") - .addToHasPart(file1) - .addToHasPart(file2) - .addAuthor("a1") - .addAuthor("a2") - .build(); - - assertTrue(rootDataEntity.hasPart(id1)); - assertTrue(rootDataEntity.hasPart(id2)); - HelpFunctions.compareEntityWithFile(rootDataEntity, "/json/entities/data/root.json"); - } - - @Test - void testSerializationMinimalExample() throws IOException, IllegalArgumentException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - RootDataEntity rootDataEntity = new RootDataEntity.RootDataEntityBuilder() - .addProperty("name", "Data files") - .addProperty("description", "Palliative care planning...") - .addDateTimePropertyWithExceptions("datePublished", "2024-02-09T08:21:41Z") - .addProperty("license", objectMapper.createObjectNode().put("@id", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/")) - .build(); - HelpFunctions.compareEntityWithFile(rootDataEntity, "/json/entities/data/rootMinimalExample.json"); - - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java b/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java deleted file mode 100644 index 9300e157..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.kit.datamanager.ro_crate.externalproviders; - -import java.io.IOException; - -import edu.kit.datamanager.ro_crate.HelpFunctions; -import edu.kit.datamanager.ro_crate.entities.contextual.OrganizationEntity; -import edu.kit.datamanager.ro_crate.externalproviders.organizationprovider.RorProvider; -import org.junit.jupiter.api.Assertions; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * @author Nikola Tzotchev on 11.2.2022 г. - * @version 1 - */ -public class RorProviderTest { - - @Test - void testExternalRorProvider() throws IOException { - OrganizationEntity organizationEntity = RorProvider.getOrganization("https://ror.org/04t3en479"); - assertNotNull(organizationEntity); - Assertions.assertEquals("https://ror.org/04t3en479", organizationEntity.getProperty("@id").asText()); - Assertions.assertEquals("https://ror.org/04t3en479", organizationEntity.getProperty("url").asText()); - Assertions.assertEquals("Karlsruhe Institute of Technology", organizationEntity.getProperty("name").asText()); - } - - @Test - void testInvalidRorUrl() { - assertThrows(IllegalArgumentException.class, () -> { - RorProvider.getOrganization("https://notror.org/04t3en479"); - }); - } - - @Test - void testInvalidRorId() { - OrganizationEntity organizationEntity = RorProvider.getOrganization("https://ror.org/42"); - assertNull(organizationEntity); - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java b/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java deleted file mode 100644 index 246aca0f..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package edu.kit.datamanager.ro_crate.other; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; -import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; - -import org.junit.jupiter.api.Test; -import io.json.compare.JSONCompare; - -import java.io.IOException; -import java.io.InputStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class UtilFunctionsTest { - - @Test - void testUnwrapSimple() throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - InputStream fileJson = - UtilFunctionsTest.class.getResourceAsStream("/json/unwrap/simple.json"); - - JsonNode node = JsonUtilFunctions.unwrapSingleArray(objectMapper.readTree(fileJson)); - JsonNode expectedResult = objectMapper.readTree( - UtilFunctionsTest.class.getResourceAsStream("/json/unwrap/simpleResult.json") - ); - JSONCompare.assertMatches(node, expectedResult); - } - - @Test - void testGettingIdEntriesFromJsonProperty() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode = objectMapper.createObjectNode(); - jsonNode.put("@id", "string1"); - - var result = JsonUtilFunctions.getIdPropertiesFromProperty(jsonNode); - assertEquals(1, result.size()); - assertEquals("string1", result.stream().findFirst().get()); - - - ArrayNode arrayNode = objectMapper.createArrayNode(); - arrayNode.add(objectMapper.createObjectNode().put("@id", "string1")); - arrayNode.add(objectMapper.createObjectNode().put("@id", "string2")); - - result = JsonUtilFunctions.getIdPropertiesFromProperty(arrayNode); - assertEquals(2, result.size()); - assertTrue(result.contains("string1")); - assertTrue(result.contains("string2")); - } - - @Test - void testGettingIdFromObject() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode = objectMapper.createObjectNode(); - - jsonNode.put("random", "not id"); - jsonNode.set("other", objectMapper.createArrayNode().add("kdfjdlk").add("kdfjalkfj")); - - jsonNode.set("test", objectMapper.createObjectNode().put("@id", "test1")); - - jsonNode.set("another", objectMapper.createObjectNode().put("@id", "test2")); - - jsonNode.set("singleArray", objectMapper.createArrayNode().add(objectMapper.createObjectNode().put("@id", "test3"))); - jsonNode.set("multipleArrayEl", objectMapper.createArrayNode() - .add(objectMapper.createObjectNode().put("@id", "test4")) - .add(objectMapper.createObjectNode().put("@id", "test5"))); - - var set = JsonUtilFunctions.getIdPropertiesFromJsonNode(jsonNode); - - assertEquals(5, set.size()); - - for (int i = 1 ; i <= 5 ; i++) { - assertTrue(set.contains("test"+i)); - } - } -} diff --git a/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java b/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java deleted file mode 100644 index 68094231..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java +++ /dev/null @@ -1,47 +0,0 @@ -package edu.kit.datamanager.ro_crate.reader; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.JsonNode; - -import edu.kit.datamanager.ro_crate.Crate; - -/** - * These reading tests are specific for reading 1.2 compliant crates. - * - * They will refer to 1.2-DRAFT as long as the specification is not final. - * Base for these tests are usually examples from the specification, but may - * contain modifications if no example for a certain property is given. - * - * Current specification: https://www.researchobject.org/ro-crate/1.2-DRAFT/ - */ -public class RoCrateReaderSpec12Test { - - /** - * Test reading a minimal crate with multiple conformsTo values, as described in - * https://www.researchobject.org/ro-crate/1.2-DRAFT/profiles.html#declaring-conformance-of-an-ro-crate-profile - */ - @Test - void testReadingCrateWithConformsToArray() throws IOException { - String path = this.getClass().getResource("/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array").getPath(); - Crate crate = Readers.newFolderReader().readCrate(path); - JsonNode conformsTo = crate.getJsonDescriptor().getProperty("conformsTo"); - assertTrue(conformsTo.isArray()); - assertEquals(2, conformsTo.size()); - // filter the list to only contain the two expected values and expect no changes - long numFiltered = StreamSupport.stream(conformsTo.spliterator(), false) - .filter(node -> { - String asText = node.path("@id").asText(); - return asText.equals("https://w3id.org/ro/crate/1.2-DRAFT") - || asText.equals("https://example.com/my/profile/1.0-TEST"); - }) - .count(); - assertEquals(2, numFiltered); - } -} 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 deleted file mode 100644 index 3c88ef07..00000000 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ /dev/null @@ -1,406 +0,0 @@ -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 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; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static edu.kit.datamanager.ro_crate.util.Graph.*; -import static org.junit.jupiter.api.Assertions.*; - -class RoCrateMetadataGenerationTest { - - private final String currentVersionId = new ProvenanceManager().getLibraryId(); - - 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); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode graph = rootNode.get("@graph"); - - // Find ro-crate-java entity - 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"); - - // Find CreateAction entity - JsonNode createActionEntity = findEntityByType(graph, "CreateAction"); - assertNotNull(createActionEntity, "CreateAction entity should exist"); - assertNotNull(createActionEntity.get("startTime"), "CreateAction should have startTime"); - assertEquals(this.currentVersionId, 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(); - 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); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); - - 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"); - } - - @Test - 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); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode graph = rootNode.get("@graph"); - - // Get both entities - JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); - 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(this.currentVersionId, 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"); - } - - @Test - 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); - - 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")); - HelpFunctions.prettyPrintJsonString(metadata); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode graph = rootNode.get("@graph"); - - // Get ro-crate-java entity - JsonNode roCrateJavaEntity = findEntityById(graph, this.currentVersionId); - 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, "CreateAction"); - 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(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(this.currentVersionId, 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(); - // 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(createTime.compareTo(updateTime2) < 0, - "Second update should be after creation"); - } - - @Test - 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")); - HelpFunctions.prettyPrintJsonString(metadata); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); - - // 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(); - 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")); - HelpFunctions.prettyPrintJsonString(metadata); - - // Parse metadata file - JsonNode rootNode = objectMapper.readTree(metadata); - JsonNode roCrateJavaEntity = findEntityById(rootNode.get("@graph"), this.currentVersionId); - - // 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"); - } - - @Test - 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) - Writers.newFolderWriter() - .withAutomaticProvenance(null) - .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, this.currentVersionId), - "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()); - validateCrate(modifiedCrate); - modifiedCrate.getRootDataEntity().addProperty("description", "Modified crate"); - validateCrate(modifiedCrate); - - // 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, this.currentVersionId); - 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(this.currentVersionId, - 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").isObject(), - "ro-crate-java should have a single reference to an UpdateAction"); - assertEquals(updateAction.get("@id").asText(), - roCrateJavaEntity.get("Action").get("@id").asText(), - "ro-crate-java should reference the UpdateAction"); - } - - @Test - 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()); - Thread.sleep(10); - - // 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()); - - // 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, this.currentVersionId); - //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"); - } -}