From 295a46a9da07c8e2e5e5a754706cb16d6f1a62cb Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Fri, 23 Jun 2023 14:27:11 +0200 Subject: [PATCH] feat: support draft features on existing crates --- .../edu/kit/datamanager/ro_crate/Crate.java | 29 +++++ .../edu/kit/datamanager/ro_crate/RoCrate.java | 104 +++++++++++++----- .../entities/contextual/JsonDescriptor.java | 10 ++ .../ro_crate/special/CrateVersion.java | 2 +- .../ro_crate/crate/BuilderSpec12Test.java | 62 ++++++++++- 5 files changed, 176 insertions(+), 31 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java b/src/main/java/edu/kit/datamanager/ro_crate/Crate.java index 9095aad..9ab1634 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/Crate.java @@ -3,6 +3,7 @@ import java.io.File; import java.util.Collection; import java.util.List; +import java.util.Optional; import edu.kit.datamanager.ro_crate.context.CrateMetadataContext; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; @@ -10,6 +11,7 @@ import edu.kit.datamanager.ro_crate.entities.data.DataEntity; import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; import edu.kit.datamanager.ro_crate.preview.CratePreview; +import edu.kit.datamanager.ro_crate.special.CrateVersion; /** * An interface describing an ROCrate. @@ -18,6 +20,33 @@ * @version 1 */ public interface Crate { + + /** + * Read version from the crate descriptor and return it as a class + * representation. + * + * NOTE: If there is not version in the crate, it does not comply with the + * specification. + * + * @return the class representation indication the version of this crate, if + * available. + */ + public Optional getVersion(); + + /** + * Returns strings indicating the conformance of a crate with other + * specifications than the RO-Crate version. + * + * If you need the crate version too, refer to {@link #getVersion()}. + * + * 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. + */ + public Collection getProfiles(); + CratePreview getPreview(); void setMetadataContext(CrateMetadataContext metadataContext); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java index 878f47f..43cc2d8 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -1,5 +1,6 @@ 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; @@ -25,7 +26,12 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + /** * The class that represents a single ROCrate. @@ -101,6 +107,38 @@ public RoCrate(RoCrateBuilder roCrateBuilder) { 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()) + .filter(txt -> !CrateVersion.fromSpecUri(txt).isPresent()) + .collect(Collectors.toSet()); + } else { + return Collections.emptySet(); + } + } + @Override public String getJsonMetadata() { ObjectMapper objectMapper = MyObjectMapper.getMapper(); @@ -319,40 +357,51 @@ public RoCrate build() { } /** - * Builder for Crates, supporting all Features from v1.1 on. - */ - public static class BuilderV1p1 extends RoCrateBuilder { - // for consistency - } - - /** - * Builder for Crates, supporting all Features from v1.2 on. + * Builder for Crates, supporting features which are not in a final + * specification yet. + * + * NOTE: This will change the specification version of your crate. * - * NOTE: Changes may happen as this is a draft! + * 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 BuilderV1p2Draft extends RoCrateBuilder { + public static class BuilderWithDraftFeatures extends RoCrateBuilder { - JsonDescriptor.Builder descriptorBuilder = new JsonDescriptor.Builder() - .setVersion(CrateVersion.V1P2_DRAFT); + JsonDescriptor.Builder descriptorBuilder = new JsonDescriptor.Builder(); /** - * A default constructor without any params where the root data entity will be - * plain. + * {@inheritDoc} + * @see RoCrateBuilder#RoCrateBuilder() */ - public BuilderV1p2Draft() { - this.payload = new RoCratePayload(); - this.untrackedFiles = new ArrayList<>(); - this.metadataContext = new RoCrateMetadataContext(); - rootDataEntity = new RootDataEntity.RootDataEntityBuilder() - .build(); - jsonDescriptor = new JsonDescriptor.Builder() - .setVersion(CrateVersion.V1P2_DRAFT) - .build(); + public BuilderWithDraftFeatures() { + super(); + } + + /** + * {@inheritDoc} + * @param name {@inheritDoc} + * @param description {@inheritDoc} + */ + public BuilderWithDraftFeatures(String name, String description) { + super(); + } + + /** + * {@inheritDoc} + * @param crate {@inheritDoc} + */ + 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 creates. + * 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. @@ -360,8 +409,11 @@ public BuilderV1p2Draft() { * @param specification a specification or profile this crate conforms to. * @return the builder */ - public BuilderV1p2Draft alsoConformsTo(URI specification) { - descriptorBuilder.addConformsTo(specification); + 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/contextual/JsonDescriptor.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java index e044408..8bb9129 100644 --- 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 @@ -7,6 +7,7 @@ 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; @@ -45,6 +46,15 @@ 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; 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 index 15deff7..18b5f1a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java @@ -45,7 +45,7 @@ public enum CrateVersion { * https://w3id.org/ro/crate/1.1 * @return the matching CrateVersion enum, if the URI matches any. Empty if not. */ - public Optional fromSpecUri(String conformsTo) { + public static Optional fromSpecUri(String conformsTo) { return Optional.ofNullable(crateVersionOfConformsTo(conformsTo)); } 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 index 90924c7..ee0d72e 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java @@ -1,10 +1,12 @@ 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.net.URI; import java.net.URISyntaxException; +import java.util.Collection; import org.junit.jupiter.api.Test; @@ -12,17 +14,69 @@ 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.FolderReader; +import edu.kit.datamanager.ro_crate.reader.RoCrateReader; +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.BuilderV1p2Draft() - .alsoConformsTo(new URI("https://w3id.org/ro/wfrun/process/0.1")) - .alsoConformsTo(new URI("https://example.com/myprofile/1.0")) - .build(); + 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 { + String path = this.getClass().getResource("/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array").getPath(); + RoCrate crate = new RoCrateReader(new FolderReader()).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 + newProfileState.contains(profile1.toString()); + newProfileState.contains(profile2.toString()); + // old profiles are present + assertEquals( + 0, + existingProfiles.stream() + .filter(txt -> !newProfileState.contains(txt)) + .count() + ); + } }