From 90e50488a31541e8d15fdefc055f35f3844df285 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 14 Oct 2026 09:59:55 +0200 Subject: [PATCH 01/21] Add golden round-trip tests as conversion safety net Lock the library's observable behavior before the Jackson-free-core conversion: - byte-level golden files for getJsonMetadata() of freshly read crates, canonicalized by sorting @graph entities by @id (entity order is unspecified in the library and varies across JVM runs) - structural write/read idempotence check reusing the suite's comparator - data-entity set must survive the round trip unchanged Goldens can be regenerated with: ./gradlew test -Drocrate.golden.update=true (the rocrate.* system properties are now propagated to the test JVM) --- build.gradle | 2 + .../ro_crate/GoldenCrateRoundTripTest.java | 150 ++++++++++++++++++ .../extendedContextExample.metadata.json | 1 + .../golden/hasPartHierarchy.metadata.json | 1 + .../golden/idrc_project.metadata.json | 1 + ...inimal-with-conformsTo-Array.metadata.json | 1 + .../golden/simple_crate.metadata.json | 1 + 7 files changed, 157 insertions(+) create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java create mode 100644 src/test/resources/golden/extendedContextExample.metadata.json create mode 100644 src/test/resources/golden/hasPartHierarchy.metadata.json create mode 100644 src/test/resources/golden/idrc_project.metadata.json create mode 100644 src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json create mode 100644 src/test/resources/golden/simple_crate.metadata.json diff --git a/build.gradle b/build.gradle index 0eb49487..a5db63da 100644 --- a/build.gradle +++ b/build.gradle @@ -178,6 +178,8 @@ test { useJUnitPlatform() dependsOn generateVersionPropsTest finalizedBy jacocoTestReport + // make flags like -Drocrate.golden.update=true visible to the test JVM + systemProperties System.getProperties().findAll { it.key.toString().startsWith('rocrate.') } } jacocoTestReport { diff --git a/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java b/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java new file mode 100644 index 00000000..fcfe642c --- /dev/null +++ b/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java @@ -0,0 +1,150 @@ +package edu.kit.datamanager.ro_crate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +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 JsonProcessingException { + JsonNode root = MyObjectMapper.getMapper().readTree(metadata); + JsonNode 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 + ) + ); + ((ArrayNode) graph).removeAll(); + entities.forEach(((ArrayNode) graph)::add); + } + return MyObjectMapper.getMapper().writeValueAsString(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/src/test/resources/golden/extendedContextExample.metadata.json b/src/test/resources/golden/extendedContextExample.metadata.json new file mode 100644 index 00000000..34e0d425 --- /dev/null +++ b/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/src/test/resources/golden/hasPartHierarchy.metadata.json b/src/test/resources/golden/hasPartHierarchy.metadata.json new file mode 100644 index 00000000..7e0634e7 --- /dev/null +++ b/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/src/test/resources/golden/idrc_project.metadata.json b/src/test/resources/golden/idrc_project.metadata.json new file mode 100644 index 00000000..885aa1fc --- /dev/null +++ b/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/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json b/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json new file mode 100644 index 00000000..c5beefb0 --- /dev/null +++ b/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/src/test/resources/golden/simple_crate.metadata.json b/src/test/resources/golden/simple_crate.metadata.json new file mode 100644 index 00000000..9bb21126 --- /dev/null +++ b/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 From 7a91f3a7acd4a695261c79a92cf9db897181e9d2 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 14 Oct 2026 12:34:18 +0200 Subject: [PATCH 02/21] Add neutral Jackson-free JSON tree model and adapter SPI Introduce edu.kit.datamanager.ro_crate.json: - JsonValue tree model (JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull, JsonMissing) mirroring the Jackson node API the library uses: same method names (put/set/get/path/fields/asText/...), insertion order, deep structural equality - semantics verified empirically against Jackson 2.22.1 and locked by parity tests: class-strict number equality, order-insensitive object equality, MissingNode.asText(default) semantics, readTree keeps single-element arrays - JsonAdapter SPI (parse/write/node factories) with Jackson2JsonAdapter discovered via ServiceLoader; serialization delegated to Jackson 2 so output stays byte-identical to the pre-conversion library No existing library code is changed yet. --- .../kit/datamanager/ro_crate/json/Json.java | 79 +++++++ .../ro_crate/json/JsonAdapter.java | 30 +++ .../datamanager/ro_crate/json/JsonArray.java | 102 +++++++++ .../ro_crate/json/JsonBoolean.java | 52 +++++ .../ro_crate/json/JsonMissing.java | 48 +++++ .../datamanager/ro_crate/json/JsonNull.java | 48 +++++ .../datamanager/ro_crate/json/JsonNumber.java | 61 ++++++ .../datamanager/ro_crate/json/JsonObject.java | 201 ++++++++++++++++++ .../datamanager/ro_crate/json/JsonString.java | 48 +++++ .../datamanager/ro_crate/json/JsonValue.java | 111 ++++++++++ .../json/jackson2/Jackson2JsonAdapter.java | 197 +++++++++++++++++ ....kit.datamanager.ro_crate.json.JsonAdapter | 1 + .../ro_crate/json/NeutralJsonModelTest.java | 153 +++++++++++++ .../Jackson2JsonAdapterParityTest.java | 83 ++++++++ 14 files changed, 1214 insertions(+) create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/Json.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java create mode 100644 src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java create mode 100644 src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java create mode 100644 src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java b/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java new file mode 100644 index 00000000..5ce98fbd --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java @@ -0,0 +1,79 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +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() { + var candidates = ServiceLoader.load(JsonAdapter.class).iterator(); + if (!candidates.hasNext()) { + throw new IllegalStateException( + "No JsonAdapter found on the classpath. Add the ro-crate-java " + + "jackson2 or jackson3 module as a dependency."); + } + JsonAdapter discovered = candidates.next(); + if (candidates.hasNext()) { + throw new IllegalStateException( + "Multiple JsonAdapters found on the classpath; ro-crate-java " + + "supports only one JSON backend at a time."); + } + 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); + } +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java new file mode 100644 index 00000000..58169acf --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java @@ -0,0 +1,30 @@ +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; +} diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java new file mode 100644 index 00000000..5905644a --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java @@ -0,0 +1,102 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * 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) { + items.add(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) { + return items.get(index); + } + + public int size() { + return items.size(); + } + + public boolean isEmpty() { + return items.isEmpty(); + } + + @Override + public Iterator iterator() { + return List.copyOf(items).iterator(); + } + + // -- 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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java new file mode 100644 index 00000000..dfb21fb5 --- /dev/null +++ b/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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java new file mode 100644 index 00000000..173bc0ec --- /dev/null +++ b/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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java new file mode 100644 index 00000000..11fd51ee --- /dev/null +++ b/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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java new file mode 100644 index 00000000..e2e44bb5 --- /dev/null +++ b/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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java new file mode 100644 index 00000000..7c6de221 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java @@ -0,0 +1,201 @@ +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; + +/** + * 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. */ + public List fieldNames() { + 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(); + } + + 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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java new file mode 100644 index 00000000..317f774d --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java @@ -0,0 +1,48 @@ +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 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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java new file mode 100644 index 00000000..830db18d --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java @@ -0,0 +1,111 @@ +package edu.kit.datamanager.ro_crate.json; + +import java.io.IOException; +import java.util.Iterator; + +/** + * 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 path(String)} on non-container nodes: always the + * missing value. Containers override this. + */ + public JsonValue path(String name) { + return JsonMissing.INSTANCE; + } + + /** + * 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; + } + + /** + * Iterates over the elements of an array, mirroring Jackson. Calling this on + * any other type throws {@link UnsupportedOperationException}. + */ + @Override + public Iterator iterator() { + throw new UnsupportedOperationException( + "Iteration is only supported on arrays" + ); + } + + /** 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/json/jackson2/Jackson2JsonAdapter.java b/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java new file mode 100644 index 00000000..dabe771f --- /dev/null +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java @@ -0,0 +1,197 @@ +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)); + } + + /** 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/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter b/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter new file mode 100644 index 00000000..e5e0a8aa --- /dev/null +++ b/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/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java b/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java new file mode 100644 index 00000000..8946318c --- /dev/null +++ b/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.fieldNames().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/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java new file mode 100644 index 00000000..fe9879d6 --- /dev/null +++ b/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()); + } +} From 86f840efa4edb594701a26928820b94918c457f4 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 14 Oct 2026 22:31:50 +0200 Subject: [PATCH 03/21] refactor!: port main source to the neutral JSON model The library core no longer touches Jackson types: all production code now works on the neutral JSON model from edu.kit.datamanager.ro_crate.json. Entity serialization is done by explicit toJson() implementations, replacing the Jackson bean serialization with its custom serializers (ObjectNodeSerializer, HasPartSerializer) and the MyObjectMapper configuration, which are all removed. The Jackson semantics the core relied on (scalar unwrapping of single-element sets, MissingNode/NullNode asText behaviour, object equality ignoring order) are now mirrored by the neutral model. BREAKING CHANGE: public API types change from Jackson tree nodes to the neutral JSON model. Users of the historical coordinates continue via the ro-crate-java-jackson2 adapter, which keeps Jackson 2 underneath. --- .../edu/kit/datamanager/ro_crate/Crate.java | 4 + .../edu/kit/datamanager/ro_crate/RoCrate.java | 1162 ++++++++-------- .../context/CrateMetadataContext.java | 7 +- .../context/RoCrateMetadataContext.java | 149 ++- .../ro_crate/entities/AbstractEntity.java | 1163 +++++++++-------- .../entities/contextual/ActionEntity.java | 4 + .../entities/contextual/ActionStatus.java | 4 + .../entities/contextual/ActionType.java | 4 + .../entities/contextual/ContextualEntity.java | 4 + .../entities/contextual/JsonDescriptor.java | 14 +- .../contextual/OrganizationEntity.java | 4 + .../entities/contextual/PersonEntity.java | 4 + .../entities/contextual/PlaceEntity.java | 4 + .../ro_crate/entities/data/DataEntity.java | 7 +- .../ro_crate/entities/data/DataSetEntity.java | 245 ++-- .../ro_crate/entities/data/FileEntity.java | 4 + .../entities/data/RootDataEntity.java | 4 + .../entities/data/WorkflowEntity.java | 4 + .../serializers/HasPartSerializer.java | 48 - .../serializers/ObjectNodeSerializer.java | 78 -- .../entities/validation/EntityValidation.java | 9 +- .../validation/EntityValidationStrategy.java | 9 +- .../validation/JsonSchemaValidation.java | 75 +- .../dataentities/ImportFromDataCite.java | 340 ++--- .../dataentities/ImportFromZenodo.java | 22 +- .../organizationprovider/RorProvider.java | 126 +- .../personprovider/OrcidProvider.java | 38 +- .../hierarchy/HierarchyRecognition.java | 4 + .../hierarchy/HierarchyRecognitionConfig.java | 4 + .../hierarchy/HierarchyRecognitionResult.java | 4 + .../ro_crate/hierarchy/package-info.java | 5 +- .../kit/datamanager/ro_crate/json/Json.java | 14 +- .../ro_crate/json/JsonAdapter.java | 4 +- .../datamanager/ro_crate/json/JsonArray.java | 16 + .../datamanager/ro_crate/json/JsonObject.java | 18 +- .../datamanager/ro_crate/json/JsonString.java | 11 + .../datamanager/ro_crate/json/JsonValue.java | 63 + .../json/jackson2/Jackson2JsonAdapter.java | 7 + .../ro_crate/objectmapper/MyObjectMapper.java | 27 - .../ro_crate/payload/CratePayload.java | 7 +- .../ro_crate/payload/EntityObserver.java | 4 + .../ro_crate/payload/Observer.java | 4 + .../ro_crate/payload/RoCratePayload.java | 43 +- .../ro_crate/preview/AutomaticPreview.java | 4 + .../ro_crate/preview/CratePreview.java | 4 + .../ro_crate/preview/CustomPreview.java | 371 +++--- .../ro_crate/preview/CustomPreviewModel.java | 4 + .../ro_crate/preview/PreviewGenerator.java | 4 + .../ro_crate/preview/StaticPreview.java | 4 + .../ro_crate/reader/CrateReader.java | 681 +++++----- .../ro_crate/reader/FolderReader.java | 4 + .../reader/GenericReaderStrategy.java | 9 +- .../ro_crate/reader/ReadFolderStrategy.java | 19 +- .../ro_crate/reader/ReadZipStrategy.java | 50 +- .../reader/ReadZipStreamStrategy.java | 246 ++-- .../ro_crate/reader/ReaderStrategy.java | 4 + .../datamanager/ro_crate/reader/Readers.java | 4 + .../ro_crate/reader/RoCrateReader.java | 4 + .../ro_crate/reader/ZipReader.java | 4 + .../ro_crate/special/CrateVersion.java | 4 + .../ro_crate/special/IdentifierUtils.java | 4 + .../ro_crate/special/JsonUtilFunctions.java | 34 +- .../ClasspathPropertiesVersionProvider.java | 4 + .../ro_crate/util/FileSystemUtil.java | 4 + .../kit/datamanager/ro_crate/util/Graph.java | 21 +- .../ro_crate/util/VersionProvider.java | 4 + .../ro_crate/util/ZipStreamUtil.java | 4 + .../validation/JsonSchemaValidation.java | 38 +- .../ro_crate/validation/Validator.java | 4 + .../validation/ValidatorStrategy.java | 4 + .../ro_crate/writer/CrateWriter.java | 4 + .../ro_crate/writer/ElnFormatWriter.java | 4 + .../ro_crate/writer/FolderWriter.java | 4 + .../writer/GenericWriterStrategy.java | 4 + .../ro_crate/writer/ProvenanceManager.java | 4 + .../ro_crate/writer/RoCrateWriter.java | 4 + .../ro_crate/writer/WriteFolderStrategy.java | 12 +- .../ro_crate/writer/WriteZipStrategy.java | 4 + .../writer/WriteZipStreamStrategy.java | 14 +- .../ro_crate/writer/WriterStrategy.java | 4 + .../datamanager/ro_crate/writer/Writers.java | 4 + .../ro_crate/writer/ZipWriter.java | 4 + 82 files changed, 2924 insertions(+), 2454 deletions(-) delete mode 100644 src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/HasPartSerializer.java delete mode 100644 src/main/java/edu/kit/datamanager/ro_crate/entities/serializers/ObjectNodeSerializer.java delete mode 100644 src/main/java/edu/kit/datamanager/ro_crate/objectmapper/MyObjectMapper.java 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 652ccdcc..49897133 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/Crate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/Crate.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate; +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.File; import java.util.Collection; 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 cb960b4a..abb640b9 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -1,14 +1,7 @@ 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; @@ -16,7 +9,13 @@ 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.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.JsonArray; +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; @@ -43,651 +42,634 @@ */ public class RoCrate implements Crate { - protected final CratePayload roCratePayload; - protected CrateMetadataContext metadataContext; - protected CratePreview roCratePreview; - protected RootDataEntity rootDataEntity; - protected ContextualEntity jsonDescriptor; - - protected Collection untrackedFiles; + 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() { + 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(); /** - * 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. + * 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. */ - protected boolean isImported = false; - - @Override - public RoCrate markAsImported() { - this.isImported = true; - return this; + 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); } - @Override - public boolean isImported() { - return this.isImported; + /** + * 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); } - @Override - public CratePreview getPreview() { - return this.roCratePreview; + /** + * 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(); } - public void setRoCratePreview(CratePreview preview) { - this.roCratePreview = preview; + /** + * 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); } - @Override - public void setMetadataContext(CrateMetadataContext metadataContext) { - this.metadataContext = metadataContext; + public RoCrateBuilder addName(String name) { + this.rootDataEntity.addProperty("name", name); + return this; } - @Override - public String getMetadataContextValueOf(String key) { - return this.metadataContext.getValueOf(key); + /** + * 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; } - @Override - public Set getMetadataContextKeys() { - return this.metadataContext.getKeys(); + public RoCrateBuilder addDescription(String description) { + this.rootDataEntity.addProperty(PROPERTY_DESCRIPTION, description); + return this; } - @Override - public Map getMetadataContextPairs() { - return this.metadataContext.getPairs(); + /** + * 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 ContextualEntity getJsonDescriptor() { - return jsonDescriptor; - } + 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." + ); + } - @Override - public void setJsonDescriptor(ContextualEntity jsonDescriptor) { - this.jsonDescriptor = jsonDescriptor; - } + this.metadataContext.checkEntity(entity); - @Override - public RootDataEntity getRootDataEntity() { - return rootDataEntity; + if (parentEntity instanceof DataSetEntity) { + ((DataSetEntity) parentEntity).addToHasPart(entity.getId()); + } else { + parentEntity.addProperty("hasPart", entity.getId()); + } + this.payload.addDataEntity(entity); } - @Override - public void setRootDataEntity(RootDataEntity rootDataEntity) { - this.rootDataEntity = rootDataEntity; + public RoCrateBuilder addContextualEntity( + ContextualEntity contextualEntity + ) { + this.metadataContext.checkEntity(contextualEntity); + this.payload.addContextualEntity(contextualEntity); + return this; } /** - * Default constructor for creation of an empty crate. + * Setting the license of the crate. + * + * @param license the license to set. + * @return this builder. */ - public RoCrate() { - this.roCratePayload = new RoCratePayload(); - this.untrackedFiles = new HashSet<>(); - this.metadataContext = new RoCrateMetadataContext(); - rootDataEntity = new RootDataEntity.RootDataEntityBuilder().build(); - jsonDescriptor = new JsonDescriptor(); + 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; } /** - * A constructor for creating the crate using a Crate builder for easier - * creation. + * Setting the license of the crate using only a license identifier. * - * @param roCrateBuilder the builder to use. + * @param licenseId the licenses identifier. Should be a resolveable + * URI. + * @return the builder */ - 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); + public RoCrateBuilder setLicense(String licenseId) { + ContextualEntity licenseEntity = + new ContextualEntity.ContextualEntityBuilder().setId(licenseId).build(); + this.setLicense(licenseEntity); + return this; } /** - * {@inheritDoc} - *

- * Note: This will also link the DataEntity to the root node using the root - * nodes hasPart property. + * Adds a property with date time format. The property should match the + * ISO 8601 date format. * - * @param entity the DataEntity to add to this crate. + * @param dateValue time string in ISO 8601 format + * @return this builder + * @throws IllegalArgumentException if format is not ISO 8601 */ - @Override - public void addDataEntity(DataEntity entity) { - this.metadataContext.checkEntity(entity); - this.roCratePayload.addDataEntity(entity); - this.rootDataEntity.addToHasPart(entity.getId()); + public RoCrateBuilder addDatePublishedWithExceptions(String dateValue) + throws IllegalArgumentException { + this.rootDataEntity.addDateTimePropertyWithExceptions( + "datePublished", + dateValue + ); + return this; } - @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() - ); + public RoCrateBuilder setContext(CrateMetadataContext context) { + this.metadataContext = context; + return this; } - @Override - public void setUntrackedFiles(Collection files) { - this.untrackedFiles = files; + public RoCrateBuilder addUrlToContext(String url) { + this.metadataContext.addToContextFromUrl(url); + return this; } - @Override - public void deleteValuePairFromContext(String key) { - this.metadataContext.deleteValuePairFromContext(key); + public RoCrateBuilder addValuePairToContext(String key, String value) { + this.metadataContext.addToContext(key, value); + return this; } - @Override - public void deleteUrlFromContext(String key) { - this.metadataContext.deleteUrlFromContext(key); + public RoCrateBuilder setPreview(CratePreview preview) { + this.preview = preview; + return this; } - @Override - public void addFromCollection( - Collection entities - ) { - this.roCratePayload.addEntities(entities); + public RoCrateBuilder addUntrackedFile(File file) { + this.untrackedFiles.add(file); + return this; } - @Override - public void addItemFromDataCite(String locationUrl) { - ImportFromDataCite.addDataCiteToCrate(locationUrl, 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 { - @Override - public Collection getUntrackedFiles() { - return this.untrackedFiles; + /** + * @see RoCrateBuilder#RoCrateBuilder() + */ + public BuilderWithDraftFeatures() { + super(); } - @Override - public HierarchyRecognitionResult createDataEntityFileStructure( - boolean addInverseRelationships + /** + * @see RoCrateBuilder#RoCrateBuilder(String, String, String, String) + */ + public BuilderWithDraftFeatures( + String name, + String description, + String datePublished, + String licenseId ) { - HierarchyRecognitionConfig config = new HierarchyRecognitionConfig() - .withSetInverseRelationships(addInverseRelationships); - return this.createDataEntityFileStructure(config); + super(name, description, datePublished, licenseId); } - @Override - public HierarchyRecognitionResult createDataEntityFileStructure( - HierarchyRecognitionConfig config + /** + * @see RoCrateBuilder#RoCrateBuilder(String, String, String, + * ContextualEntity) + */ + public BuilderWithDraftFeatures( + String name, + String description, + String datePublished, + ContextualEntity licenseId ) { - return new HierarchyRecognition(this, config).buildHierarchy(); + super(name, description, datePublished, licenseId); } /** - * The inner class builder for the easier creation of a ROCrate. + * @see RoCrateBuilder#RoCrateBuilder(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); - } + public BuilderWithDraftFeatures(RoCrate crate) { + super(crate); + this.descriptorBuilder = new JsonDescriptor.Builder(crate); } /** - * Builder for Crates, supporting features which are not in a final - * specification yet. - *

- * NOTE: This will change the specification version of your crate. + * Indicate this crate also conforms to the given specification, in + * addition to the version this builder adds. *

- * 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. + * 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 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; - } - } + 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/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java index dfe6548e..23faee80 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java @@ -1,6 +1,9 @@ package edu.kit.datamanager.ro_crate.context; +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.node.ObjectNode; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; @@ -17,7 +20,7 @@ */ public interface CrateMetadataContext { - ObjectNode getContextJsonEntity(); + JsonObject getContextJsonEntity(); boolean checkEntity(AbstractEntity entity); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java index d76cf86b..884a2f41 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java @@ -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/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index e978b199..cba9abe6 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -1,22 +1,16 @@ 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.DataSetEntity; 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.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.JsonUtilFunctions; import edu.kit.datamanager.ro_crate.special.IdentifierUtils; - +import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,620 +23,703 @@ */ 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); + /** + * 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( + 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. + */ + 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); + } } - - private void notifyObservers() { - this.observers.forEach(observer -> observer.update(this.getId())); + } + + 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) { + // mirrors the former Jackson valueToTree(types): a string array in + // the iteration order of the underlying set + JsonArray typeArray = Json.arrayNode(); + for (String type : this.types) { + typeArray.add(type); + } + this.properties.set("@type", typeArray); } - - /** - * 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); - } - } + return properties; + } + + 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(); } - - public Set getLinkedTo() { - return linkedTo; + } + + /** + * 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(); } - /** - * Returns the types of this entity. - * - * @return a set of type strings. - */ - public Set getTypes() { - return types; + JsonObject newIdObject = Json.objectNode().put("@id", id); + if ( + currentValue == null || currentValue.isNull() || currentValue.isMissing() + ) { + return Optional.ofNullable(newIdObject); } - /** - * 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; + 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(); } - public JsonNode getProperty(String propertyKey) { - return this.properties.get(propertyKey); + 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); + JsonArray typeArray = Json.arrayNode(); + for (String typeValue : this.types) { + typeArray.add(typeValue); + } + this.properties.set("@type", typeArray); + } + + /** + * 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(); } + } - /** - * 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); + /** + * 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<>(); } - @JsonIgnore - public String getId() { - JsonNode id = this.properties.get("@id"); - return id == null ? null : id.asText(); + protected String getId() { + return this.id; } /** - * Set all the properties from a Json object to the Entity. The entities are - * first validated to filter any invalid entity properties. + * 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 obj the object that contains all the json properties that should - * be added. + * @param id the String representing the id. + * @return the generic builder. */ - public void setProperties(JsonNode obj) { - // validate whole entity - if (entityValidation.entityValidation(obj)) { - this.properties = obj.deepCopy(); - this.notifyObservers(); + 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); } - } - - protected void setId(String id) { - this.properties.put("@id", id); + } + return self(); } /** - * removes one property from an entity. + * Adding a type to the builder types. * - * @param key the key of the entity, which will be removed. + * @param type the type to add. + * @return the generic builder. */ - public void removeProperty(String key) { - this.getProperties().remove(key); - this.notifyObservers(); + public T addType(String type) { + if (this.types == null) { + this.types = new HashSet<>(); + } + if (type != null && !type.isEmpty()) { + this.types.add(type); + } + return self(); } /** - * Removes a collection of properties from an entity. + * Adds multiple types in one call. * - * @param keys collection of keys, which will be removed. + * @param types the types to add. + * @return the generic builder. */ - public void removeProperties(Collection keys) { - this.getProperties().remove(keys); - this.notifyObservers(); + public T addTypes(Collection types) { + if (this.types == null) { + this.types = new HashSet<>(); + } + this.types.addAll(types); + return self(); } /** - * Adds a property to the entity. + * Adds a property with date time format. The property should match the ISO 8601 + * date format. *

- * It may override values, if the key already exists. + * Same as {@link #addProperty(String, String)} but with internal check. * - * @param key the key of the property. - * @param value value of the property. + * @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 void addProperty(String key, String value) { - if (key != null && value != null) { - this.properties.put(key, value); - this.notifyObservers(); - } + public T addDateTimePropertyWithExceptions(String key, String value) + throws IllegalArgumentException { + if (value != null) { + checkFormatISO8601(value); + this.properties.put(key, value); + } + return self(); } /** - * Adds a property to the entity. - *

- * It may override values, if the key already exists. + * Adding a property to the builder. * - * @param key the key of the property. - * @param value value of the property. + * @param key the key of the property in a string. + * @param value the JsonValue value of te property. + * @return the generic builder. */ - public void addProperty(String key, long value) { - if (key != null) { - this.properties.put(key, value); - this.notifyObservers(); - } + public T addProperty(String key, JsonValue value) { + if (AbstractEntity.addProperty(this.properties, key, value)) { + this.relatedItems.addAll( + JsonUtilFunctions.getIdPropertiesFromProperty(value) + ); + } + return self(); } /** - * Adds a property to the entity. - *

- * It may override values, if the key already exists. + * Adding a property to the builder. * - * @param key the key of the property. - * @param value value of the property. + * @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 void addProperty(String key, double value) { - if (key != null) { - this.properties.put(key, value); - this.notifyObservers(); - } + public T addProperty(String key, String value) { + this.properties.put(key, value); + return self(); } - /** - * 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(); - } + public T addProperty(String key, int value) { + this.properties.put(key, value); + return self(); } - 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; + public T addProperty(String key, double value) { + this.properties.put(key, value); + return self(); } - /** - * 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(); + public T addProperty(String key, boolean value) { + this.properties.put(key, value); + return self(); } /** - * Merges the given id into the current value, - * using this representation: {"@id" : "id"}. + * ID properties are often used when referencing other entities within + * the ROCrate. This method adds automatically such one. *

- * 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. + * Instead of {@code "name": "id" } + * this will add {@code "name" : {"@id": "id"} } + * + * Does nothing if name or id are null. * - * @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! + * @param name the name of the ID property. + * @param id the ID. + * @return the generic builder */ - 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); - } + 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(); } /** - * Adds everything from the properties to the property "name" as id. + * This is another way of adding the ID property, this time the whole + * other Entity is provided. * - * @param name the key of the property. - * @param properties a collection containing all the id as String. + * @param name the name of the property. + * @param entity the other entity that is referenced. + * @return the generic builder. */ - 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(); + public T addIdProperty(String name, AbstractEntity entity) { + if (entity != null) { + return addIdProperty(name, entity.getId()); + } + return self(); } /** - * Adding new type to the property (which may have multiple such ones). + * This adds multiple id entities to a single key. * - * @param type the String representing the type. + * @param name the name of the property. + * @param entities the Collection containing the multiple entities. + * @return the generic builder. */ - 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); + public T addIdFromCollectionOfEntities( + String name, + Collection entities + ) { + if (entities != null) { + for (var e : entities) { + addIdProperty(name, e); + } + } + return self(); } /** - * Checks if the date matches the ISO 8601 date format. + * Deprecated. Equivalent to {@link #setAllIfValid(JsonObject)}. * - * @param date the date as a string - * @throws IllegalArgumentException if format does not match + * @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) */ - 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"); - } + @Deprecated(since = "2.1.0", forRemoval = true) + public T setAll(JsonObject properties) { + return setAllIfValid(properties); } /** - * Adds a property with date time format. The property should match the ISO 8601 - * date format. + * This sets everything from a json object to the property, + * if the result is valid. Otherwise, it will do nothing. *

- * Same as {@link #addProperty(String, String)} but with internal check. + * 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 key key of the property (e.g. datePublished) - * @param value time string in ISO 8601 format - * @throws IllegalArgumentException if format is not ISO 8601 + * @param properties the Json representing all the properties. + * @return the generic builder, either including all given properties + * or unchanged. */ - public void addDateTimePropertyWithExceptions(String key, String value) throws IllegalArgumentException { - if (value != null) { - checkFormatISO8601(value); - this.properties.put(key, value); - this.notifyObservers(); - } + public T setAllIfValid(JsonObject properties) { + if (AbstractEntity.entityValidation.entityValidation(properties)) { + this.properties = properties; + this.relatedItems.addAll( + JsonUtilFunctions.getIdPropertiesFromJsonNode(properties) + ); + } + return self(); } /** - * This a builder inner class that should allow for an easier creating of - * entities. + * 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 The type of the child builders so that they to can use the - * methods provided here. + * @param properties the Json representing all the properties. + * @return the generic builder with all properties added. */ - 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(); + 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/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 index 8136052a..e8d644c3 100644 --- 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 @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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.Collection; import java.util.HashSet; 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 index f107dcac..8a1c7a06 100644 --- 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 @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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; public enum ActionStatus { ACTIVE_ACTION_STATUS("http://schema.org/ActiveActionStatus"), 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 index 02cff8dd..e84a48ef 100644 --- 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 @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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; /** * Enumeration class representing action types. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java index 1897b6df..1165b045 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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.entities.AbstractEntity; 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 88aaf89e..f654d868 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 @@ -1,14 +1,15 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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.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 { @@ -78,13 +79,12 @@ public JsonDescriptor build() { 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(); + JsonArray array = Json.arrayNode(); array.add( - mapper.createObjectNode().put("@id", this.version.conformsTo)); + Json.objectNode().put("@id", this.version.conformsTo)); for (String other : this.otherConformsToValues) { array.add( - mapper.createObjectNode().put("@id", other)); + Json.objectNode().put("@id", other)); } ContextualEntityBuilder entityBuilder = staticPropertiesPrefilledBuilder() .addProperty(CONFORMS_TO, array); diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java index 71f90618..9e13aafe 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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; /** * A helping class for creating organization entities. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java index 84cb8731..a72af132 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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; /** * A helping class for creating a Person Entity. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java index bf5dd3e3..03ece75e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.entities.contextual; +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; /** * A helping class for the Place entity creation. 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 index 829ee9c2..39e2da46 100644 --- 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 @@ -1,6 +1,9 @@ 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 com.fasterxml.jackson.annotation.JsonIgnore; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; @@ -19,7 +22,6 @@ */ public class DataEntity extends AbstractEntity { - @JsonIgnore private Path path; /** @@ -58,7 +60,6 @@ public void addAuthorId(String id) { * * @return the path to the file this entity represents. */ - @JsonIgnore public Path getPath() { return path; } 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 index 8030c04c..51f701ba 100644 --- 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 @@ -1,12 +1,9 @@ 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 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; @@ -19,128 +16,144 @@ */ 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 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); + } - public void removeFromHasPart(String str) { - this.hasPart.remove(str); + abstract static class AbstractDataSetBuilder> extends AbstractDataEntityBuilder { + + Set hasPart; + + public AbstractDataSetBuilder() { + this.hasPart = new HashSet<>(); } - public void addToHasPart(String id) { - if (id != null && !id.isEmpty()) { - this.hasPart.add(id); - } + public T setHasPart(Set hasPart) { + this.hasPart = hasPart; + return self(); } - /** - * 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); + public T addToHasPart(DataEntity dataEntity) { + if (dataEntity != null) { + this.hasPart.add(dataEntity.getId()); + this.relatedItems.add(dataEntity.getId()); + } + return self(); } - /** - * 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); + public T addToHasPart(String dataEntity) { + if (dataEntity != null) { + this.hasPart.add(dataEntity); + this.relatedItems.add(dataEntity); + } + return self(); } - 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(); + @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(); } - /** - * Builder for the helping class DataSetEntity. - */ - public static final class DataSetBuilder extends AbstractDataSetBuilder { + @Override + public abstract DataSetEntity build(); + } - @Override - public DataSetBuilder self() { - return this; - } + /** + * 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); - } + @Override + public DataSetEntity build() { + return new DataSetEntity(this); } + } } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java index 44a2a4e1..0b4d4f10 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java @@ -1,4 +1,8 @@ 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; /** * A helping class representing a File entity. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java index 01133112..c75e4433 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java @@ -1,4 +1,8 @@ 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.nio.file.Paths; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java index 27f86e32..69ab78eb 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java @@ -1,4 +1,8 @@ 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.ArrayList; import java.util.List; 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/EntityValidation.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java index aa54c3ca..b5193f3e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java @@ -1,7 +1,10 @@ package edu.kit.datamanager.ro_crate.entities.validation; +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; /** * Class used for validation of entities before their creation. @@ -14,11 +17,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/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java index 644b49e7..8dc71c2a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java @@ -1,13 +1,16 @@ package edu.kit.datamanager.ro_crate.entities.validation; +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; /** * Interface for the strategy of the entity validation. */ public interface EntityValidationStrategy { - boolean validateEntity(JsonNode entity); + boolean validateEntity(JsonValue entity); - boolean validateFieldOfEntity(JsonNode field); + boolean validateFieldOfEntity(JsonValue field); } 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 index 18e9624a..51850bf8 100644 --- 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 @@ -1,15 +1,11 @@ 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 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; @@ -17,15 +13,23 @@ /** * 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 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; @@ -34,7 +38,9 @@ public class JsonSchemaValidation implements EntityValidationStrategy { * Default constructor that uses the default schemas. */ public JsonSchemaValidation() { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V201909 + ); try { this.entitySchema = factory.getSchema(entitySchemaDefault.toURI()); this.entityFieldSchema = factory.getSchema(fieldSchemaDefault.toURI()); @@ -49,17 +55,27 @@ public JsonSchemaValidation() { * @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); + 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(JsonNode entity) { - Set errors = this.entitySchema.validate(entity); + 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."); + 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; } @@ -67,18 +83,17 @@ public boolean validateEntity(JsonNode entity) { } @Override - public boolean validateFieldOfEntity(JsonNode field) { - Set errors = this.entityFieldSchema.validate(field); + public boolean validateFieldOfEntity(JsonValue field) { + Set errors = this.entityFieldSchema.validate( + Jackson2JsonAdapter.toJackson(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."); + 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/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java b/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/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/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java index 4a99ee8a..ba69e8e7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java @@ -1,17 +1,18 @@ package edu.kit.datamanager.ro_crate.externalproviders.dataentities; +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.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 java.io.ByteArrayInputStream; import java.io.IOException; @@ -67,21 +68,20 @@ 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"); 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( @@ -89,8 +89,8 @@ private static void addToCrateFromZotero(String url, Crate crate) { 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); @@ -100,7 +100,7 @@ private static void addToCrateFromZotero(String url, Crate crate) { doc.toString().getBytes(StandardCharsets.UTF_8)))).get(); - jsonNode = (ObjectNode) objectMapper.readTree(finalVersion.toString()); + jsonNode = (JsonObject) Json.parse(finalVersion.toString()); } catch (IOException | JsonLdError e) { e.printStackTrace(); return; @@ -112,12 +112,12 @@ 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/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 index 350fdad6..fb436773 100644 --- 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 @@ -1,15 +1,13 @@ 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 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.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; @@ -23,63 +21,75 @@ */ public class RorProvider { - private static final Logger logger = LoggerFactory.getLogger(RorProvider.class); - - private RorProvider() { - } + private static final Logger logger = LoggerFactory.getLogger( + RorProvider.class + ); - /** - * 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); + private RorProvider() {} - 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); + /** + * 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); - 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); - } + 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(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(); + 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/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java index 17207993..ae639804 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java @@ -1,13 +1,12 @@ 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.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; import java.io.IOException; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; @@ -48,23 +47,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( + new JsonSchemaValidation() + ); var itr = jsonNode.fields(); while (itr.hasNext()) { var element = itr.next(); 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 index 46a278c7..6fbd2c80 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.hierarchy; +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.Crate; import edu.kit.datamanager.ro_crate.entities.data.DataEntity; 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 index c37372d0..c872e539 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.hierarchy; +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; /** * Configuration class for automatic hierarchy recognition. 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 index ce4eab50..bb6c7113 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.hierarchy; +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.entities.data.DataEntity; import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java index 5d8f119f..b5692cc7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java @@ -6,4 +6,7 @@ * 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;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; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java b/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java index 5ce98fbd..803405a1 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java @@ -41,14 +41,16 @@ private static JsonAdapter discoverAdapter() { var candidates = ServiceLoader.load(JsonAdapter.class).iterator(); if (!candidates.hasNext()) { throw new IllegalStateException( - "No JsonAdapter found on the classpath. Add the ro-crate-java " - + "jackson2 or jackson3 module as a dependency."); + "No JsonAdapter found on the classpath. Add the ro-crate-java " + + "jackson2 or jackson3 module as a dependency." + ); } JsonAdapter discovered = candidates.next(); if (candidates.hasNext()) { throw new IllegalStateException( - "Multiple JsonAdapters found on the classpath; ro-crate-java " - + "supports only one JSON backend at a time."); + "Multiple JsonAdapters found on the classpath; ro-crate-java " + + "supports only one JSON backend at a time." + ); } return discovered; } @@ -76,4 +78,8 @@ public static JsonValue parse(InputStream stream) throws IOException { 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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java index 58169acf..0a6fffad 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java @@ -13,7 +13,6 @@ * (see {@link Json}). */ public interface JsonAdapter { - JsonObject objectNode(); JsonArray arrayNode(); @@ -27,4 +26,7 @@ public interface JsonAdapter { /** 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/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java index 5905644a..bc0fe2b8 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java @@ -3,6 +3,7 @@ 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 @@ -53,9 +54,19 @@ public JsonValue remove(int 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(); } @@ -69,6 +80,11 @@ public Iterator iterator() { return List.copyOf(items).iterator(); } + @Override + public Stream valueStream() { + return List.copyOf(items).stream(); + } + // -- tree operations -------------------------------------------------------- @Override diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java index 7c6de221..e6175ecf 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java @@ -7,6 +7,7 @@ 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 @@ -119,8 +120,16 @@ public boolean isEmpty() { return names.isEmpty(); } - /** Property names in insertion order. */ - public List fieldNames() { + /** + * 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); } @@ -143,6 +152,11 @@ 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++) { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java index 317f774d..cc1ec2c6 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java @@ -26,6 +26,17 @@ 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; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java index 830db18d..85ed2550 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java @@ -1,7 +1,10 @@ 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 @@ -61,6 +64,42 @@ 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. @@ -69,6 +108,30 @@ 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. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java b/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java index dabe771f..72d6bf61 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java @@ -67,6 +67,13 @@ 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()) { 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/payload/CratePayload.java b/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java index 7639ef84..adaf9826 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java @@ -1,6 +1,9 @@ package edu.kit.datamanager.ro_crate.payload; +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.node.ArrayNode; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; @@ -38,7 +41,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/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java index 3119cbe4..e8c99a72 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.payload; +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; /** * Implementation of the Observer pattern, used by the deletion of entities. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java b/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java index 9b1f1758..dbffa24f 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.payload; +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; /** * Interface for the observer pattern used with the deletion of entities. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java b/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java index bc76d6a2..7460619c 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java @@ -1,15 +1,13 @@ 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.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions; - import java.util.*; import java.util.stream.Collectors; @@ -22,8 +20,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 +121,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 +140,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/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java index c403ab1c..83c5feb8 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; +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.util.ZipStreamUtil; import java.io.File; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java index e966b4af..303aa30e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; +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.File; import java.io.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 index 77300a87..aa996f79 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java @@ -1,7 +1,9 @@ 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.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.util.ZipStreamUtil; import freemarker.template.Configuration; import freemarker.template.Template; @@ -35,183 +37,216 @@ */ 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 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 { - 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); - } - } + } + + 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()); } - // 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; - } - } - } + 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); } - - 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."); + // 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; + } } - 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 - } + for (CustomPreviewModel.File file : files) { + if (file.id.equals(part.id) && file.name != null) { + part.name = file.name; + } } + } } - @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); - } + 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." + ); } - - @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 - } - } + 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 index 21dbd802..60f04d70 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; +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.List; 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 index 60e8e19e..6c1497c8 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; +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.BufferedReader; import java.io.IOException; 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 index c3a93629..41506c7c 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; +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.util.ZipStreamUtil; import java.io.File; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java index c63ba21a..6df48f80 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -1,8 +1,5 @@ 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; @@ -10,13 +7,14 @@ 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.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 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; @@ -24,6 +22,8 @@ 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 @@ -39,324 +39,385 @@ */ public class CrateReader { - private static final Logger logger = LoggerFactory.getLogger(CrateReader.class); + 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 { + /** + * 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); - } + 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(); + } + + /** + * 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); } - - 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); - }); + 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() + ); } - return result; + } } - protected String unpackId(JsonNode node) { - if (node.isTextual()) { - return node.asText(); - } else /*if (node.isObject())*/ { - return node.path(PROP_ID).asText(); - } + 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 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 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(); + protected Set getDataEntityIds(RootDataEntity root, JsonValue graph) { + if (root == null) { + return Set.of(); } - - /** - * 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); - }); + 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); }); } - - /** - * 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(); + return result; + } + + protected String unpackId(JsonValue node) { + if (node.isTextual()) { + return node.asText(); + } else /*if (node.isObject())*/ { + return node.path(PROP_ID).asText(); } - - 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; + } + + 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() + ); - private void setCrateDescriptor(RoCrate crate, JsonNode descriptor) { - ContextualEntity descriptorEntity = new ContextualEntity.ContextualEntityBuilder() - .setAllUnsafe(descriptor.deepCopy()) - .build(); - crate.setJsonDescriptor(descriptorEntity); + 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/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java index c6268ac1..d81df5d7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.reader; +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; /** * A class for reading a crate from a folder. 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 index 6a9f4bd4..80465bfe 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java @@ -1,6 +1,9 @@ package edu.kit.datamanager.ro_crate.reader; +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.node.ObjectNode; import java.io.File; import java.io.IOException; @@ -15,9 +18,9 @@ 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 + * @return the parsed metadata.json as JsonObject */ - ObjectNode readMetadataJson(SOURCE_TYPE location) throws IOException; + JsonObject readMetadataJson(SOURCE_TYPE location) throws IOException; /** * Read the content from the given location. 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 index 927d77de..59170ded 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java @@ -1,9 +1,9 @@ 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 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.File; import java.io.IOException; import java.nio.file.Path; @@ -17,11 +17,12 @@ 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(); + 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; } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java index 5a2474b5..ca40bdb1 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java @@ -1,18 +1,18 @@ 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.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; 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 +42,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 +108,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/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java index 3cc41086..363355da 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java @@ -1,9 +1,11 @@ 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.JsonArray; +import edu.kit.datamanager.ro_crate.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +import edu.kit.datamanager.ro_crate.util.FileSystemUtil; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -11,8 +13,6 @@ 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; @@ -47,130 +47,148 @@ * * @author jejkal */ -public class ReadZipStreamStrategy implements GenericReaderStrategy { +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; + 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() {} + /** + * 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; - } + /** + * 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 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 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; - } + /** + * @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); + /**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]; + 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); - } - } - } + 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 + ); } - this.isExtracted = true; - // register deletion on exit - FileUtils.forceDeleteOnExit(folder); + 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); - } + @Override + public JsonObject 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(); + 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(); + @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/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java index 2029fe9c..905bf8f2 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.reader; +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; /** * Interface for the strategy for the reader class. 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 index 83a43701..e14396c9 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.reader; +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.InputStream; import java.nio.file.Path; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java index d11177c1..46e8a708 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.reader; +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; /** * This class allows reading crates from the outside into the library in order diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java index d92faf94..d380258f 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.reader; +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.nio.file.Path; 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 74d8ca1c..fd0f0cfb 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 @@ -1,4 +1,8 @@ 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 java.net.URI; import java.net.URISyntaxException; 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 index 4fec8e58..12a792e7 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java @@ -1,4 +1,8 @@ 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 java.net.MalformedURLException; import java.net.URL; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java b/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/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/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java index 812772cd..646d7b8a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.util; +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.io.InputStream; 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 index 82c39529..fcc4d1c4 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.util; +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 org.apache.commons.io.FileUtils; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java index 45161f16..23b42479 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -1,6 +1,9 @@ package edu.kit.datamanager.ro_crate.util; +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 java.util.stream.StreamSupport; @@ -21,10 +24,10 @@ private Graph() { * * @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. + * @return The entity as a JsonValue if found, null otherwise. */ - public static JsonNode findEntityById(JsonNode graph, String id) { - for (JsonNode entity : graph) { + public static JsonValue findEntityById(JsonValue graph, String id) { + for (JsonValue entity : graph) { if (entity.has("@id") && entity.get("@id").asText().equals(id)) { return entity; } @@ -37,9 +40,9 @@ public static JsonNode findEntityById(JsonNode graph, String id) { * * @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. + * @return The entity as a JsonValue if found, null otherwise. */ - public static JsonNode findEntityByType(JsonNode graph, String type) { + public static JsonValue findEntityByType(JsonValue graph, String type) { return StreamSupport.stream(graph.spliterator(), false) .filter(entity -> entity.path("@type").asText().equals(type)) .findFirst() @@ -51,11 +54,11 @@ public static JsonNode findEntityByType(JsonNode graph, String 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. + * @return An array of JsonValue containing all entities of the specified type. */ - public static JsonNode[] findEntitiesByType(JsonNode graph, String type) { + public static JsonValue[] findEntitiesByType(JsonValue graph, String type) { return StreamSupport.stream(graph.spliterator(), false) .filter(entity -> entity.path("@type").asText().equals(type)) - .toArray(JsonNode[]::new); + .toArray(JsonValue[]::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 index cd5c4ad2..07b38395 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.util; +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; public interface VersionProvider { /** 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 index b8eb2ef1..d8125b18 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.util; +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.File; import java.io.FileInputStream; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java b/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/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/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java b/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java index d408b97e..8cd05721 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.validation; +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.Crate; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java index 94e68906..c5c31057 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.validation; +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.Crate; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java index d708ddd7..9828daac 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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.Crate; import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation; 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 index bd6310cd..09bd1074 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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; /** * An Interface for {@link GenericWriterStrategy} implementations which support writing diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java index 1426414e..304f6b79 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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; /** * A class for writing a crate to a folder. diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java index d06301a0..3ead88f9 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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.Crate; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java index fd4a2c50..2c00040a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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.Crate; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java index 9337d400..4a2f5b5a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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; /** * The class used for writing (exporting) crates. The class uses a strategy 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 index 0816cd59..e3b446ef 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java @@ -1,10 +1,11 @@ package edu.kit.datamanager.ro_crate.writer; +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.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; @@ -46,9 +47,8 @@ public WriteFolderStrategy disablePreview() { 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); + 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"); 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 index 003f7973..0dccbc5e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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.Crate; 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 index 8dfb6adf..546aa57d 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java @@ -1,11 +1,12 @@ package edu.kit.datamanager.ro_crate.writer; +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.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; @@ -96,10 +97,9 @@ private void saveMetadataJson(Crate crate, ZipOutputStream zipStream, String pre // 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); + // 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]; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java index 12459673..e94a9a60 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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; /** * Strategy for writing of crates. 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 index d1a41d66..eb011346 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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.OutputStream; diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java index c9c0735e..5e8cfd14 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java @@ -1,4 +1,8 @@ package edu.kit.datamanager.ro_crate.writer; +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; /** * Implementation of the writing strategy to provide a way of writing crates to From 91bdfacd7d695f217973f16f0cf4cba816f7f1a9 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 14 Oct 2026 23:03:03 +0200 Subject: [PATCH 04/21] test: port HelpFunctions to the neutral JSON model The shared test helper now compares JSON via the string form of the neutral trees (produced by the active adapter), so json-compare no longer receives Jackson node objects. --- .../datamanager/ro_crate/HelpFunctions.java | 324 +++++++++--------- .../ro_crate/json/NeutralJsonModelTest.java | 2 +- 2 files changed, 172 insertions(+), 154 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java b/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java index 24a57a07..b640351c 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java @@ -1,19 +1,13 @@ 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 static org.junit.jupiter.api.Assertions.assertEquals; -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.json.Json; +import edu.kit.datamanager.ro_crate.json.JsonValue; 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; @@ -22,157 +16,181 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; +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); - 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 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 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 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 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); + } + + 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); } - - /** - * 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 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; } - - 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; } - 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()); - } - }); + } 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/json/NeutralJsonModelTest.java b/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java index 8946318c..e650d1eb 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java @@ -17,7 +17,7 @@ class NeutralJsonModelTest { void objectPreservesInsertionOrderAndOverwritesInPlace() { JsonObject object = JsonObject.create(); object.put("b", "second").put("a", "first"); - assertEquals("b", object.fieldNames().get(0)); + assertEquals("b", object.names().get(0)); object.put("b", "changed"); assertEquals(2, object.size()); assertEquals("changed", object.get("b").asText()); From 5e9794b3f1c08a48816a444238fa133c8e947da0 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 15 Oct 2026 10:59:10 +0200 Subject: [PATCH 05/21] test: port test suite to the neutral JSON model All tests now build their expectations with the neutral JSON model. The golden parity test intentionally keeps Jackson 2 as the reference, locking the adapter output against the pre-split library output. --- .../ro_crate/GoldenCrateRoundTripTest.java | 25 +- .../context/RoCrateMetadataContextTest.java | 172 ++-- .../ro_crate/crate/BuilderSpec12Test.java | 134 +-- .../ro_crate/crate/BuilderTest.java | 5 +- .../ro_crate/crate/TestRemoveAddContext.java | 5 +- .../ro_crate/crate/TestRemoveAddEntity.java | 367 +++---- .../crate/validation/ValidationTest.java | 47 +- .../ro_crate/entities/AbstractEntityTest.java | 239 +++-- .../contextual/ContextualEntityTest.java | 193 ++-- .../entities/data/DataEntityTest.java | 450 +++++---- .../entities/data/DataSetEntityTest.java | 10 +- .../entities/data/RootDataEntityTest.java | 9 +- .../externalproviders/DataCiteImportTest.java | 59 +- .../ro_crate/other/UtilFunctionsTest.java | 71 +- .../reader/RoCrateReaderSpec12Test.java | 57 +- .../writer/RoCrateMetadataGenerationTest.java | 932 ++++++++++-------- 16 files changed, 1531 insertions(+), 1244 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java b/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java index fcfe642c..1fded937 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java @@ -3,11 +3,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; 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.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; @@ -59,11 +59,11 @@ static Stream fixtureCrates() { * 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 JsonProcessingException { - JsonNode root = MyObjectMapper.getMapper().readTree(metadata); - JsonNode graph = root.path("@graph"); + 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<>(); + List entities = new ArrayList<>(); graph.forEach(entities::add); entities.sort( Comparator.comparing( @@ -71,10 +71,13 @@ static String canonicalize(String metadata) throws JsonProcessingException { String.CASE_INSENSITIVE_ORDER ) ); - ((ArrayNode) graph).removeAll(); - entities.forEach(((ArrayNode) graph)::add); + JsonArray graphArray = (JsonArray) graph; + while (!graphArray.isEmpty()) { + graphArray.remove(0); + } + entities.forEach(graphArray::add); } - return MyObjectMapper.getMapper().writeValueAsString(root); + return Json.write(root); } @ParameterizedTest diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index 1fb7b574..4329bf84 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -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/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java index 270127af..55343b9c 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 @@ -4,79 +4,85 @@ 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.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(); - JsonNode conformsTo = crate.getJsonDescriptor().getProperty("conformsTo"); - assertTrue(conformsTo.isArray()); - // one version and two profiles - assertEquals(1 + 2, conformsTo.size()); - } + 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(); - @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())); + // 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() + ); - // 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() - ); - } + // 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/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/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/TestRemoveAddContext.java b/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/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/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java index bc959c29..037d306f 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java @@ -1,178 +1,213 @@ package edu.kit.datamanager.ro_crate.crate; -import com.fasterxml.jackson.core.JsonProcessingException; +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 org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - 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 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); - } + + @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/validation/ValidationTest.java b/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/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/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java index 678642ba..efc32a7b 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java @@ -1,111 +1,148 @@ 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 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; -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"); - } + @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/ContextualEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java index 7bc868f3..c9e4baf0 100644 --- 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 @@ -1,16 +1,15 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -import java.io.IOException; +import static org.junit.jupiter.api.Assertions.*; -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 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 org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - /** * @author Nikola Tzotchev on 5.2.2022 г. * @version 1 @@ -26,124 +25,126 @@ public class ContextualEntityTest { 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(); + .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(); + .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"); + HelpFunctions.compareEntityWithFile( + place, + "/json/entities/contextual/place.json" + ); } @Test - void testAddAllValidCase() throws JsonProcessingException { + 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(); + .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); + { + "@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(); + .setAllIfValid(properties) + .build(); assertEquals(second.getProperties(), first.getProperties()); } @Test - void testAddAllInvalidCase() throws JsonProcessingException { + 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(); + .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); + { + "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(); + .setId("second") + .setAllIfValid(properties) + .build(); assertNotEquals(second.getProperties(), first.getProperties()); - ObjectNode empty = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .build() - .getProperties(); + JsonObject empty = new ContextualEntity.ContextualEntityBuilder() + .setId("second") + .build() + .getProperties(); assertEquals(empty, second.getProperties()); } @Test - void testAddAllUnsafeDoesInvalidCase() throws JsonProcessingException { + 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(); + .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); + { + "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(); + .setId("second") + .setAllUnsafe(properties) + .build(); assertNotEquals(second.getProperties(), first.getProperties()); - ObjectNode empty = new ContextualEntity.ContextualEntityBuilder() - .setId("second") - .build() - .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/data/DataEntityTest.java b/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java index f551c891..bddc3ba7 100644 --- 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 @@ -1,239 +1,257 @@ 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 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.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.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; -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); - } + @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/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 index 5ae4142d..92d81972 100644 --- 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 @@ -1,12 +1,13 @@ 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 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; @@ -78,8 +79,7 @@ void testDirWithHasPartDeserialization() throws IOException { @Test void testWebDir() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode propertyValue = objectMapper.createObjectNode(); + JsonObject propertyValue = Json.objectNode(); propertyValue.put("@id", "http://example.com/downloads/2020/lots_of_little_files.zip"); DataSetEntity dir = new DataSetEntity.DataSetBuilder() 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 index 76033f70..ffd0f789 100644 --- 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 @@ -1,12 +1,14 @@ 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 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; @@ -53,12 +55,11 @@ void testSerialization() throws IOException { @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/")) + .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/externalproviders/DataCiteImportTest.java b/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/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/other/UtilFunctionsTest.java b/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java index 246aca0f..3640e735 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java @@ -1,51 +1,47 @@ 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; -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.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; +import org.junit.jupiter.api.Test; public class UtilFunctionsTest { @Test void testUnwrapSimple() throws IOException { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - InputStream fileJson = - UtilFunctionsTest.class.getResourceAsStream("/json/unwrap/simple.json"); + 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") + JsonValue node = JsonUtilFunctions.unwrapSingleArray(Json.parse(fileJson)); + JsonValue expectedResult = Json.parse( + UtilFunctionsTest.class.getResourceAsStream( + "/json/unwrap/simpleResult.json" + ) ); - JSONCompare.assertMatches(node, expectedResult); + JSONCompare.assertMatches(node.toString(), expectedResult.toString()); } @Test void testGettingIdEntriesFromJsonProperty() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode = objectMapper.createObjectNode(); + JsonObject jsonNode = Json.objectNode(); 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")); + 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()); @@ -55,27 +51,32 @@ void testGettingIdEntriesFromJsonProperty() { @Test void testGettingIdFromObject() { - ObjectMapper objectMapper = MyObjectMapper.getMapper(); - ObjectNode jsonNode = objectMapper.createObjectNode(); + JsonObject jsonNode = Json.objectNode(); jsonNode.put("random", "not id"); - jsonNode.set("other", objectMapper.createArrayNode().add("kdfjdlk").add("kdfjalkfj")); + jsonNode.set("other", Json.arrayNode().add("kdfjdlk").add("kdfjalkfj")); - jsonNode.set("test", objectMapper.createObjectNode().put("@id", "test1")); + jsonNode.set("test", Json.objectNode().put("@id", "test1")); - jsonNode.set("another", objectMapper.createObjectNode().put("@id", "test2")); + jsonNode.set("another", Json.objectNode().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"))); + 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)); + 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 index 68094231..00b0289f 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java @@ -3,45 +3,46 @@ 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; -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); - } + /** + * 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/writer/RoCrateMetadataGenerationTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java index 3c88ef07..7d8a877e 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java @@ -1,406 +1,558 @@ 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 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 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.*; +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 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)); + 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") - @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"); - } + 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" + ); + } } From d3a3e6a33363e7872c8fab10b00cc9c20359fbd0 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 15 Oct 2026 22:55:58 +0200 Subject: [PATCH 06/21] fix: mirror Jackson semantics found by the test suite Three behavior mismatches between the neutral model and Jackson, all verified empirically against Jackson 2.22.1: - JsonValue.iterator() mirrors Jackson node iteration: scalars iterate nothing, objects their values, arrays their elements - JsonArray.add(null) stores JsonNull (Jackson's ArrayNode.add does the same: a null argument becomes a NullNode) - entity @type mirrors the former valueToTree(types) with the legacy mapper's WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED: a single type serializes to a plain string, multiple types to an array Full test suite (314 tests incl. golden round-trip and adapter parity tests) passes. --- .../ro_crate/entities/AbstractEntity.java | 29 +++++++++++-------- .../datamanager/ro_crate/json/JsonArray.java | 3 +- .../datamanager/ro_crate/json/JsonValue.java | 9 +++--- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index cba9abe6..c96fdd59 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -97,17 +97,26 @@ public Set getTypes() { */ public JsonObject getProperties() { if (this.types != null) { - // mirrors the former Jackson valueToTree(types): a string array in - // the iteration order of the underlying set - JsonArray typeArray = Json.arrayNode(); - for (String type : this.types) { - typeArray.add(type); - } - this.properties.set("@type", typeArray); + 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); } @@ -410,11 +419,7 @@ public void addType(String type) { this.types = new HashSet<>(); } this.types.add(type); - JsonArray typeArray = Json.arrayNode(); - for (String typeValue : this.types) { - typeArray.add(typeValue); - } - this.properties.set("@type", typeArray); + this.properties.set("@type", typesNode()); } /** diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java index bc0fe2b8..9553dbb2 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java @@ -21,7 +21,8 @@ public static JsonArray create() { // -- construction / mutation ------------------------------------------------ public JsonArray add(JsonValue value) { - items.add(value); + // mirrors Jackson's ArrayNode.add: a null argument becomes a null node + items.add(value == null ? JsonNull.INSTANCE : value); return this; } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java index 85ed2550..1eec7e51 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java @@ -149,14 +149,13 @@ public String asText(String defaultValue) { } /** - * Iterates over the elements of an array, mirroring Jackson. Calling this on - * any other type throws {@link UnsupportedOperationException}. + * 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() { - throw new UnsupportedOperationException( - "Iteration is only supported on arrays" - ); + return Collections.emptyIterator(); } /** Compact JSON representation, delegated to the active {@link JsonAdapter}. */ From ca34fb99963c30edd0b0df6b4dc580d69c86542b Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 15 Oct 2026 23:28:13 +0200 Subject: [PATCH 07/21] cleanup(core): remove unused imports from Graph --- .../kit/datamanager/ro_crate/util/Graph.java | 89 +++++++++---------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java index 23b42479..58f0ad17 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -1,9 +1,4 @@ package edu.kit.datamanager.ro_crate.util; -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.stream.StreamSupport; @@ -15,50 +10,50 @@ */ public class Graph { - private Graph() { - // Private constructor to prevent instantiation - } + 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 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 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); - } + /** + * 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); + } } From 1461ded09d196344b1c0e57ad180e032e9ee5813 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Sun, 18 Oct 2026 14:38:06 +0200 Subject: [PATCH 08/21] build: split the build into core, jackson2 and jackson3 modules Restructure the single Gradle project into three modules: - ro-crate-java-core: the Jackson-free domain plus the neutral JSON model and the JsonAdapter/validation SPIs. The main compile classpath has no JSON library dependency (enforced by the module boundary). The JSON backend and the JSON-schema validation strategies are discovered via ServiceLoader (new Validation facade in core). - ro-crate-java-jackson2: the Jackson 2 adapter and networknt 1.5.x validation. Publishes under the historical coordinates (edu.kit.datamanager:ro-crate-java) so existing consumers keep resolving the same artifact, transitively pulling the core. - ro-crate-java-jackson3: new backend on tools.jackson 3.2.1 (the version the networknt 3.0.x validator requires), with a Jackson3JsonAdapter, registered via ServiceLoader. This makes the library usable from Jackson 3 applications. Publishing/signing/release configuration moved into shared gradle scripts (module-common, module-publishing); the root project now only coordinates the release plugin and Nexus publishing. --- build.gradle | 200 ++--------------- core/build.gradle | 142 +++++++++++++ .../edu/kit/datamanager/ro_crate/Crate.java | 0 .../edu/kit/datamanager/ro_crate/RoCrate.java | 4 +- .../context/CrateMetadataContext.java | 0 .../context/RoCrateMetadataContext.java | 0 .../ro_crate/entities/AbstractEntity.java | 4 +- .../entities/contextual/ActionEntity.java | 0 .../entities/contextual/ActionStatus.java | 0 .../entities/contextual/ActionType.java | 0 .../entities/contextual/ContextualEntity.java | 0 .../entities/contextual/JsonDescriptor.java | 0 .../contextual/OrganizationEntity.java | 0 .../entities/contextual/PersonEntity.java | 0 .../entities/contextual/PlaceEntity.java | 0 .../ro_crate/entities/data/DataEntity.java | 0 .../ro_crate/entities/data/DataSetEntity.java | 0 .../ro_crate/entities/data/FileEntity.java | 0 .../entities/data/RootDataEntity.java | 0 .../entities/data/WorkflowEntity.java | 0 .../entities/validation/EntityValidation.java | 0 .../validation/EntityValidationStrategy.java | 0 .../dataentities/ImportFromDataCite.java | 0 .../dataentities/ImportFromZenodo.java | 0 .../organizationprovider/RorProvider.java | 0 .../personprovider/OrcidProvider.java | 4 +- .../hierarchy/HierarchyRecognition.java | 0 .../hierarchy/HierarchyRecognitionConfig.java | 0 .../hierarchy/HierarchyRecognitionResult.java | 0 .../ro_crate/hierarchy/package-info.java | 0 .../kit/datamanager/ro_crate/json/Json.java | 0 .../ro_crate/json/JsonAdapter.java | 0 .../datamanager/ro_crate/json/JsonArray.java | 0 .../ro_crate/json/JsonBoolean.java | 0 .../ro_crate/json/JsonMissing.java | 0 .../datamanager/ro_crate/json/JsonNull.java | 0 .../datamanager/ro_crate/json/JsonNumber.java | 0 .../datamanager/ro_crate/json/JsonObject.java | 0 .../datamanager/ro_crate/json/JsonString.java | 0 .../datamanager/ro_crate/json/JsonValue.java | 0 .../ro_crate/payload/CratePayload.java | 0 .../ro_crate/payload/EntityObserver.java | 0 .../ro_crate/payload/Observer.java | 0 .../ro_crate/payload/RoCratePayload.java | 0 .../ro_crate/preview/AutomaticPreview.java | 0 .../ro_crate/preview/CratePreview.java | 0 .../ro_crate/preview/CustomPreview.java | 0 .../ro_crate/preview/CustomPreviewModel.java | 0 .../ro_crate/preview/PreviewGenerator.java | 0 .../ro_crate/preview/StaticPreview.java | 0 .../ro_crate/reader/CrateReader.java | 4 +- .../ro_crate/reader/FolderReader.java | 0 .../reader/GenericReaderStrategy.java | 0 .../ro_crate/reader/ReadFolderStrategy.java | 0 .../ro_crate/reader/ReadZipStrategy.java | 0 .../reader/ReadZipStreamStrategy.java | 0 .../ro_crate/reader/ReaderStrategy.java | 0 .../datamanager/ro_crate/reader/Readers.java | 0 .../ro_crate/reader/RoCrateReader.java | 0 .../ro_crate/reader/ZipReader.java | 0 .../ro_crate/special/CrateVersion.java | 0 .../ro_crate/special/IdentifierUtils.java | 0 .../ro_crate/special/JsonUtilFunctions.java | 0 .../ClasspathPropertiesVersionProvider.java | 0 .../ro_crate/util/FileSystemUtil.java | 0 .../kit/datamanager/ro_crate/util/Graph.java | 1 + .../ro_crate/util/VersionProvider.java | 0 .../ro_crate/util/ZipStreamUtil.java | 0 .../ro_crate/validation/Validation.java | 62 ++++++ .../ro_crate/validation/Validator.java | 0 .../validation/ValidatorStrategy.java | 0 .../ro_crate/writer/CrateWriter.java | 61 ++++++ .../ro_crate/writer/ElnFormatWriter.java | 0 .../ro_crate/writer/FolderWriter.java | 0 .../writer/GenericWriterStrategy.java | 0 .../ro_crate/writer/ProvenanceManager.java | 0 .../ro_crate/writer/RoCrateWriter.java | 0 .../ro_crate/writer/WriteFolderStrategy.java | 0 .../ro_crate/writer/WriteZipStrategy.java | 0 .../writer/WriteZipStreamStrategy.java | 0 .../ro_crate/writer/WriterStrategy.java | 0 .../datamanager/ro_crate/writer/Writers.java | 0 .../ro_crate/writer/ZipWriter.java | 0 .../resources/default_context/version1.1.json | 0 .../resources/templates/custom_preview.ftl | 0 .../MultipleCratesBenchmark.java | 0 .../MultipleCratesWriteAndRead.java | 0 .../ContextualEntitiesPerformance.java | 0 .../DeletionEntitiesPerformance.java | 0 .../LocalDataEntitiesPerformance.java | 0 .../MixtureOfEntitiesPerformance.java | 0 .../RemoteDataEntitiesPerformance.java | 0 .../ro_crate/GoldenCrateRoundTripTest.java | 0 .../datamanager/ro_crate/HelpFunctions.java | 0 .../context/RoCrateMetadataContextTest.java | 0 .../ro_crate/crate/BuilderSpec12Test.java | 0 .../ro_crate/crate/BuilderTest.java | 0 .../ro_crate/crate/HasPartTest.java | 0 .../ro_crate/crate/OtherFilesTest.java | 0 .../ro_crate/crate/ReadAndWriteTest.java | 0 .../ro_crate/crate/SerializationTest.java | 0 .../ro_crate/crate/TestRemoveAddContext.java | 0 .../ro_crate/crate/TestRemoveAddEntity.java | 0 .../crate/preview/PreviewCrateTest.java | 0 .../ro_crate/crate/realexamples/RealTest.java | 0 .../crate/realexamples/WorkflowHubTest.java | 0 .../crate/validation/ValidationTest.java | 0 .../ro_crate/entities/AbstractEntityTest.java | 0 .../entities/contextual/ActionEntityTest.java | 0 .../contextual/ContextualEntityTest.java | 0 .../contextual/OrganizationEntityTest.java | 0 .../entities/contextual/PersonEntityTest.java | 0 .../entities/contextual/PlaceEntityTest.java | 0 .../entities/data/DataEntityTest.java | 0 .../entities/data/DataSetEntityTest.java | 0 .../entities/data/FileEntityTest.java | 0 .../entities/data/RootDataEntityTest.java | 0 .../entities/data/WorkflowEntityTest.java | 0 .../ExamplesOfSpecificationV1p1Test.java | 0 .../ro_crate/examples/LearnByExampleTest.java | 0 .../externalproviders/DataCiteImportTest.java | 0 .../externalproviders/OrcidProviderTest.java | 0 .../externalproviders/RorProviderTest.java | 0 .../externalproviders/ZenodoImportTest.java | 0 .../hierarchy/HierarchyRecognitionTest.java | 0 .../ro_crate/json/NeutralJsonModelTest.java | 0 .../ro_crate/other/UtilFunctionsTest.java | 0 .../ro_crate/payload/PayloadTest.java | 0 .../ro_crate/preview/PreviewTest.java | 0 .../ro_crate/reader/CommonReaderTest.java | 0 .../ro_crate/reader/ElnFileFormatTest.java | 0 .../ro_crate/reader/FolderReaderTest.java | 0 .../reader/RoCrateReaderSpec12Test.java | 0 .../reader/TestableReaderStrategy.java | 0 .../ro_crate/reader/ZipReaderTest.java | 0 .../ro_crate/reader/ZipStreamReaderTest.java | 0 .../ro_crate/special/CrateVersionTest.java | 0 .../ro_crate/special/IdentifierUtilsTest.java | 0 .../ro_crate/util/FileSystemUtilTest.java | 0 .../ro_crate/writer/CommonWriterTest.java | 0 .../ro_crate/writer/ElnFileWriterTest.java | 0 .../ro_crate/writer/FolderWriterTest.java | 0 .../writer/ProvenanceManagerTest.java | 0 .../writer/RoCrateMetadataGenerationTest.java | 0 .../writer/RoCrateWriterSpec12Test.java | 0 .../writer/TestableWriterStrategy.java | 0 .../ro_crate/writer/ZipStreamWriterTest.java | 0 .../ro_crate/writer/ZipWriterTest.java | 0 .../ro-crate-metadata.json | 0 .../crates/hasPartHierarchy/README.md | 0 .../hasPartHierarchy/ro-crate-metadata.json | 0 .../other/idrc_project/ro-crate-metadata.json | 0 .../simple_crate/ro-crate-metadata.json | 0 .../ro-crate-metadata.json | 0 .../crates/validation/workflowschema.json | 0 .../crates/workflowhub/workflow1/CHANGELOG.md | 0 .../crates/workflowhub/workflow1/README.md | 0 .../workflow1/ro-crate-metadata.json | 0 .../workflow1/ro-crate-preview.html | 0 .../workflow1/test-data/by_variant_report.tsv | 0 .../test-data/combined_variant_report.tsv | 0 .../workflow1/test-data/ont-artic.vcf | 0 .../workflow1/test-data/pe-artic.vcf | 0 .../workflow1/test-data/pe-illumina-wgs.vcf | 0 .../workflow1/test-data/se-illumina-wgs.vcf | 0 .../workflow1/variation-reporting-test.yml | 0 .../workflow1/variation-reporting.ga | 0 .../extendedContextExample.metadata.json | 0 .../golden/hasPartHierarchy.metadata.json | 0 .../golden/idrc_project.metadata.json | 0 ...inimal-with-conformsTo-Array.metadata.json | 0 .../golden/simple_crate.metadata.json | 0 .../resources/json/crate/BiggerExample.json | 0 .../test/resources/json/crate/almost-50%.json | 0 .../test/resources/json/crate/fileAndDir.json | 0 .../resources/json/crate/onlyOneFile.json | 0 .../test/resources/json/crate/simple.json | 0 .../test/resources/json/crate/simple2.json | 0 .../test/resources/json/crate/twoFiles.json | 0 .../contextual/createActionExample.json | 0 .../createActionExampleSoftware.json | 0 .../json/entities/contextual/equipment.json | 0 .../json/entities/contextual/orcidperson.json | 0 .../entities/contextual/organization.json | 0 .../json/entities/contextual/person.json | 0 .../json/entities/contextual/place.json | 0 .../json/entities/contextual/software.json | 0 .../entities/contextual/updateAction.json | 0 .../json/entities/data/directory.json | 0 .../json/entities/data/directoryWeb.json | 0 .../json/entities/data/fileEntity.json | 0 .../resources/json/entities/data/license.json | 0 .../json/entities/data/localFile.json | 0 .../entities/data/localFileWithLicense.json | 0 .../resources/json/entities/data/root.json | 0 .../entities/data/rootMinimalExample.json | 0 .../resources/json/entities/data/script1.json | 0 .../json/entities/data/workflow.json | 0 .../test/resources/json/unwrap/simple.json | 0 .../resources/json/unwrap/simpleResult.json | 0 .../complete-workflow-example.json | 0 .../file-author-location.json | 0 .../files-and-folders.json | 0 .../spec-v1.1-example-json-files/minimal.json | 0 .../web-based-data-entities.json | 0 gradle/module-common.gradle | 44 ++++ gradle/module-publishing.gradle | 67 ++++++ gradle/profile-release.gradle | 123 ----------- jackson2/build.gradle | 40 ++++ .../validation/JsonSchemaValidation.java | 0 .../json/jackson2/Jackson2JsonAdapter.java | 0 .../validation/JsonSchemaValidation.java | 0 ...tities.validation.EntityValidationStrategy | 1 + ....kit.datamanager.ro_crate.json.JsonAdapter | 0 ...ager.ro_crate.validation.ValidatorStrategy | 1 + .../main/resources/json_schemas/default.json | 0 .../entity_field_structure_schema.json | 0 .../resources/json_schemas/entity_schema.json | 0 .../Jackson2JsonAdapterParityTest.java | 0 jackson3/build.gradle | 44 ++++ .../json/jackson3/Jackson3JsonAdapter.java | 201 ++++++++++++++++++ ...tities.validation.EntityValidationStrategy | 1 + ....kit.datamanager.ro_crate.json.JsonAdapter | 1 + ...ager.ro_crate.validation.ValidatorStrategy | 1 + settings.gradle | 1 + .../ro_crate/writer/CrateWriter.java | 59 ----- .../workflowhub/workflow-109-5.crate.zip | Bin 41737 -> 0 bytes 227 files changed, 693 insertions(+), 373 deletions(-) create mode 100644 core/build.gradle rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/Crate.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/RoCrate.java (99%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java (99%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/data/FileEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntity.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidation.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/entities/validation/EntityValidationStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromDataCite.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/externalproviders/dataentities/ImportFromZenodo.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/externalproviders/personprovider/OrcidProvider.java (96%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/hierarchy/package-info.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/Json.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java (99%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/special/JsonUtilFunctions.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/util/Graph.java (97%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java (100%) create mode 100644 core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validation.java rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java (100%) create mode 100644 core/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java (100%) rename {src => core/src}/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java (100%) rename {src => core/src}/main/resources/default_context/version1.1.json (100%) rename {src => core/src}/main/resources/templates/custom_preview.ftl (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesBenchmark.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/multiplecrates/MultipleCratesWriteAndRead.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/ContextualEntitiesPerformance.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/DeletionEntitiesPerformance.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/LocalDataEntitiesPerformance.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/MixtureOfEntitiesPerformance.java (100%) rename {src => core/src}/performanceTest/java/edu/kit/datamanager/ro_crate/singlecratebenchmarks/RemoteDataEntitiesPerformance.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/BuilderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/HasPartTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/OtherFilesTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/SerializationTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddContext.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/preview/PreviewCrateTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/RealTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/realexamples/WorkflowHubTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/crate/validation/ValidationTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/contextual/OrganizationEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PersonEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/contextual/PlaceEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/data/FileEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/entities/data/WorkflowEntityTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/examples/LearnByExampleTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/externalproviders/DataCiteImportTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/externalproviders/OrcidProviderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/externalproviders/ZenodoImportTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/payload/PayloadTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/preview/PreviewTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/CommonReaderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/ElnFileFormatTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/FolderReaderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/TestableReaderStrategy.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/ZipReaderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/reader/ZipStreamReaderTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/special/CrateVersionTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/special/IdentifierUtilsTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/util/FileSystemUtilTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/ElnFileWriterTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/FolderWriterTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManagerTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriterSpec12Test.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/TestableWriterStrategy.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/ZipStreamWriterTest.java (100%) rename {src => core/src}/test/java/edu/kit/datamanager/ro_crate/writer/ZipWriterTest.java (100%) rename {src => core/src}/test/resources/crates/extendedContextExample/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/hasPartHierarchy/README.md (100%) rename {src => core/src}/test/resources/crates/hasPartHierarchy/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/other/idrc_project/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/simple_crate/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/spec-1.2-DRAFT/minimal-with-conformsTo-Array/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/validation/workflowschema.json (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/CHANGELOG.md (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/README.md (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/ro-crate-metadata.json (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/ro-crate-preview.html (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/by_variant_report.tsv (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/combined_variant_report.tsv (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/ont-artic.vcf (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/pe-artic.vcf (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/pe-illumina-wgs.vcf (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/test-data/se-illumina-wgs.vcf (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/variation-reporting-test.yml (100%) rename {src => core/src}/test/resources/crates/workflowhub/workflow1/variation-reporting.ga (100%) rename {src => core/src}/test/resources/golden/extendedContextExample.metadata.json (100%) rename {src => core/src}/test/resources/golden/hasPartHierarchy.metadata.json (100%) rename {src => core/src}/test/resources/golden/idrc_project.metadata.json (100%) rename {src => core/src}/test/resources/golden/minimal-with-conformsTo-Array.metadata.json (100%) rename {src => core/src}/test/resources/golden/simple_crate.metadata.json (100%) rename {src => core/src}/test/resources/json/crate/BiggerExample.json (100%) rename {src => core/src}/test/resources/json/crate/almost-50%.json (100%) rename {src => core/src}/test/resources/json/crate/fileAndDir.json (100%) rename {src => core/src}/test/resources/json/crate/onlyOneFile.json (100%) rename {src => core/src}/test/resources/json/crate/simple.json (100%) rename {src => core/src}/test/resources/json/crate/simple2.json (100%) rename {src => core/src}/test/resources/json/crate/twoFiles.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/createActionExample.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/createActionExampleSoftware.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/equipment.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/orcidperson.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/organization.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/person.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/place.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/software.json (100%) rename {src => core/src}/test/resources/json/entities/contextual/updateAction.json (100%) rename {src => core/src}/test/resources/json/entities/data/directory.json (100%) rename {src => core/src}/test/resources/json/entities/data/directoryWeb.json (100%) rename {src => core/src}/test/resources/json/entities/data/fileEntity.json (100%) rename {src => core/src}/test/resources/json/entities/data/license.json (100%) rename {src => core/src}/test/resources/json/entities/data/localFile.json (100%) rename {src => core/src}/test/resources/json/entities/data/localFileWithLicense.json (100%) rename {src => core/src}/test/resources/json/entities/data/root.json (100%) rename {src => core/src}/test/resources/json/entities/data/rootMinimalExample.json (100%) rename {src => core/src}/test/resources/json/entities/data/script1.json (100%) rename {src => core/src}/test/resources/json/entities/data/workflow.json (100%) rename {src => core/src}/test/resources/json/unwrap/simple.json (100%) rename {src => core/src}/test/resources/json/unwrap/simpleResult.json (100%) rename {src => core/src}/test/resources/spec-v1.1-example-json-files/complete-workflow-example.json (100%) rename {src => core/src}/test/resources/spec-v1.1-example-json-files/file-author-location.json (100%) rename {src => core/src}/test/resources/spec-v1.1-example-json-files/files-and-folders.json (100%) rename {src => core/src}/test/resources/spec-v1.1-example-json-files/minimal.json (100%) rename {src => core/src}/test/resources/spec-v1.1-example-json-files/web-based-data-entities.json (100%) create mode 100644 gradle/module-common.gradle create mode 100644 gradle/module-publishing.gradle delete mode 100644 gradle/profile-release.gradle create mode 100644 jackson2/build.gradle rename {src => jackson2/src}/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java (100%) rename {src => jackson2/src}/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java (100%) rename {src => jackson2/src}/main/java/edu/kit/datamanager/ro_crate/validation/JsonSchemaValidation.java (100%) create mode 100644 jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy rename {src => jackson2/src}/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter (100%) create mode 100644 jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy rename {src => jackson2/src}/main/resources/json_schemas/default.json (100%) rename {src => jackson2/src}/main/resources/json_schemas/entity_field_structure_schema.json (100%) rename {src => jackson2/src}/main/resources/json_schemas/entity_schema.json (100%) rename {src => jackson2/src}/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java (100%) create mode 100644 jackson3/build.gradle create mode 100644 jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java create mode 100644 jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy create mode 100644 jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter create mode 100644 jackson3/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy delete mode 100644 src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java delete mode 100644 src/test/resources/crates/workflowhub/workflow-109-5.crate.zip diff --git a/build.gradle b/build.gradle index a5db63da..83fd09b7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,205 +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.1.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.5.0" } 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.1' -} - -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.1' - // url validator - implementation group: 'commons-validator', name: 'commons-validator', version: '1.11.0' - // logging - implementation group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.18' - // 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.34") -} - -// 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 - // make flags like -Drocrate.golden.update=true visible to the test JVM - systemProperties System.getProperties().findAll { it.key.toString().startsWith('rocrate.') } -} - -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..a77251bb --- /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.1' + // 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.18' + // 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.34") + 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java similarity index 99% rename from src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java index abb640b9..2f37e127 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -22,7 +22,7 @@ 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.Validation; import edu.kit.datamanager.ro_crate.validation.Validator; import java.io.File; import java.net.URI; @@ -140,7 +140,7 @@ public RoCrate(RoCrateBuilder roCrateBuilder) { this.rootDataEntity = roCrateBuilder.rootDataEntity; this.jsonDescriptor = roCrateBuilder.descriptorBuilder.build(); this.untrackedFiles = roCrateBuilder.untrackedFiles; - Validator defaultValidation = new Validator(new JsonSchemaValidation()); + Validator defaultValidation = new Validator(Validation.crateStrategy()); defaultValidation.validate(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 100% 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 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java similarity index 99% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java index c96fdd59..cbb6e74c 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java @@ -3,7 +3,6 @@ import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity; import edu.kit.datamanager.ro_crate.entities.validation.EntityValidation; -import edu.kit.datamanager.ro_crate.entities.validation.JsonSchemaValidation; import edu.kit.datamanager.ro_crate.json.Json; import edu.kit.datamanager.ro_crate.json.JsonArray; import edu.kit.datamanager.ro_crate.json.JsonObject; @@ -11,6 +10,7 @@ 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; @@ -38,7 +38,7 @@ public class AbstractEntity { private JsonObject properties; private static final EntityValidation entityValidation = new EntityValidation( - new JsonSchemaValidation() + Validation.entityStrategy() ); /** diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionEntity.java diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionStatus.java diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/ActionType.java 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 100% 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 diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/contextual/JsonDescriptor.java 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 100% 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 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 100% 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 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 100% 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 diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntity.java 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/externalproviders/organizationprovider/RorProvider.java 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 96% 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 ae639804..43d289c0 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 @@ -2,11 +2,11 @@ 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.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.validation.Validation; import java.io.IOException; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; @@ -71,7 +71,7 @@ public static PersonEntity getPerson(String url) { jsonNode.remove("@context"); JsonObject node = Json.objectNode(); EntityValidation entityValidation = new EntityValidation( - new JsonSchemaValidation() + Validation.entityStrategy() ); var itr = jsonNode.fields(); while (itr.hasNext()) { diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionConfig.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java b/core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognitionResult.java 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/Json.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/Json.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonAdapter.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonArray.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonBoolean.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonMissing.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNull.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonNumber.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonObject.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonString.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java b/core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/json/JsonValue.java 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 100% 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 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/AutomaticPreview.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/CratePreview.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreview.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/CustomPreviewModel.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/PreviewGenerator.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java b/core/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/preview/StaticPreview.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java similarity index 99% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java index 6df48f80..1d4ae61f 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -13,7 +13,7 @@ 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.JsonSchemaValidation; +import edu.kit.datamanager.ro_crate.validation.Validation; import edu.kit.datamanager.ro_crate.validation.Validator; import java.io.File; import java.io.IOException; @@ -190,7 +190,7 @@ private RoCrate rebuildCrate( .collect(Collectors.toSet()); crate.setUntrackedFiles(untrackedFiles); - Validator defaultValidation = new Validator(new JsonSchemaValidation()); + Validator defaultValidation = new Validator(Validation.crateStrategy()); defaultValidation.validate(crate); return crate; } 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/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/GenericReaderStrategy.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadFolderStrategy.java 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStreamStrategy.java 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/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java 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 100% 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 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/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java b/core/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/special/CrateVersion.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java b/core/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/special/IdentifierUtils.java 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/util/ClasspathPropertiesVersionProvider.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/util/FileSystemUtil.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java similarity index 97% rename from src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java index 58f0ad17..7971b85e 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/util/Graph.java @@ -1,5 +1,6 @@ package edu.kit.datamanager.ro_crate.util; +import edu.kit.datamanager.ro_crate.json.JsonValue; import java.util.stream.StreamSupport; /** diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/util/VersionProvider.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java b/core/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/util/ZipStreamUtil.java 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..8aee9631 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validation.java @@ -0,0 +1,62 @@ +package edu.kit.datamanager.ro_crate.validation; + +import edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy; +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) { + var candidates = ServiceLoader.load(type).iterator(); + if (!candidates.hasNext()) { + throw new IllegalStateException( + "No " + type.getSimpleName() + " found on the classpath. Add the " + + "ro-crate-java-jackson2 or ro-crate-java-jackson3 module as a " + + "dependency."); + } + 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."); + } + 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..43bb41d7 --- /dev/null +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java @@ -0,0 +1,61 @@ +package edu.kit.datamanager.ro_crate.writer; + +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.json.JsonObject; +import edu.kit.datamanager.ro_crate.json.JsonValue; +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/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/ElnFormatWriter.java 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/ProvenanceManager.java 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 100% 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 diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStrategy.java diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java 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/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java rename to core/src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java 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/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/GoldenCrateRoundTripTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java b/core/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/HelpFunctions.java 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 100% 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 diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/BuilderSpec12Test.java 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 100% 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 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 100% 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 diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java b/core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/crate/TestRemoveAddEntity.java 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 100% 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 diff --git a/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/AbstractEntityTest.java 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/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 similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/contextual/ContextualEntityTest.java 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/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 similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataEntityTest.java diff --git a/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 similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/DataSetEntityTest.java 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/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 similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/entities/data/RootDataEntityTest.java 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 100% 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 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/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/externalproviders/RorProviderTest.java 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/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/json/NeutralJsonModelTest.java diff --git a/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/other/UtilFunctionsTest.java 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/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java b/core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/reader/RoCrateReaderSpec12Test.java 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/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/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java b/core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java rename to core/src/test/java/edu/kit/datamanager/ro_crate/writer/RoCrateMetadataGenerationTest.java 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/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/src/test/resources/golden/extendedContextExample.metadata.json b/core/src/test/resources/golden/extendedContextExample.metadata.json similarity index 100% rename from src/test/resources/golden/extendedContextExample.metadata.json rename to core/src/test/resources/golden/extendedContextExample.metadata.json diff --git a/src/test/resources/golden/hasPartHierarchy.metadata.json b/core/src/test/resources/golden/hasPartHierarchy.metadata.json similarity index 100% rename from src/test/resources/golden/hasPartHierarchy.metadata.json rename to core/src/test/resources/golden/hasPartHierarchy.metadata.json diff --git a/src/test/resources/golden/idrc_project.metadata.json b/core/src/test/resources/golden/idrc_project.metadata.json similarity index 100% rename from src/test/resources/golden/idrc_project.metadata.json rename to core/src/test/resources/golden/idrc_project.metadata.json diff --git a/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json b/core/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json similarity index 100% rename from src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json rename to core/src/test/resources/golden/minimal-with-conformsTo-Array.metadata.json diff --git a/src/test/resources/golden/simple_crate.metadata.json b/core/src/test/resources/golden/simple_crate.metadata.json similarity index 100% rename from src/test/resources/golden/simple_crate.metadata.json rename to core/src/test/resources/golden/simple_crate.metadata.json 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/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..da08cd7c --- /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.5.0" +} + +ext { + jacksonVersion = '2.22.1' + // 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/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java rename to jackson2/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/JsonSchemaValidation.java diff --git a/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 similarity index 100% rename from src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java rename to jackson2/src/main/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapter.java 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 100% 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 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/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 similarity index 100% rename from src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter rename to jackson2/src/main/resources/META-INF/services/edu.kit.datamanager.ro_crate.json.JsonAdapter 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/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 similarity index 100% rename from src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java rename to jackson2/src/test/java/edu/kit/datamanager/ro_crate/json/jackson2/Jackson2JsonAdapterParityTest.java 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/json/jackson3/Jackson3JsonAdapter.java b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java new file mode 100644 index 00000000..695091eb --- /dev/null +++ b/jackson3/src/main/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3JsonAdapter.java @@ -0,0 +1,201 @@ +package edu.kit.datamanager.ro_crate.json.jackson3; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.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 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/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/settings.gradle b/settings.gradle index 2bc86c66..b8a1965b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'ro-crate-java' +include 'core', 'jackson2', 'jackson3' 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 9828daac..00000000 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java +++ /dev/null @@ -1,59 +0,0 @@ -package edu.kit.datamanager.ro_crate.writer; -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.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/test/resources/crates/workflowhub/workflow-109-5.crate.zip b/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip deleted file mode 100644 index 6f33ca7ea9cc60c55b2e94f6a25f3140f8b4dff4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41737 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 From 737773594a2a3833a74a2f803ec661594cea4f98 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Sun, 18 Oct 2026 16:56:35 +0200 Subject: [PATCH 09/21] feat(jackson3): add Jackson 3 validation backends with golden parity The networknt 3.x validator redesigned its API (SchemaRegistry, Schema and Error instead of JsonSchemaFactory, JsonSchema and ValidationMessage); the jackson3 module implements both validation strategies on it. Schemas load via the classpath scheme, which networknt 3.x requires for resolving the relative refs between the schema files (verified empirically; file-URI loading fails in networknt 3.0.8). All three modules pass the full test suite, including the golden-file parity test proving the Jackson 3 backend produces byte-identical metadata to the original Jackson 2 library. --- .../jackson3/JsonSchemaValidation.java | 95 ++++++++++++ .../jackson3/JsonSchemaValidation.java | 89 +++++++++++ .../main/resources/json_schemas/default.json | 20 +++ .../entity_field_structure_schema.json | 46 ++++++ .../resources/json_schemas/entity_schema.json | 10 ++ .../jackson3/Jackson3BackendParityTest.java | 144 ++++++++++++++++++ 6 files changed, 404 insertions(+) create mode 100644 jackson3/src/main/java/edu/kit/datamanager/ro_crate/entities/validation/jackson3/JsonSchemaValidation.java create mode 100644 jackson3/src/main/java/edu/kit/datamanager/ro_crate/validation/jackson3/JsonSchemaValidation.java create mode 100644 jackson3/src/main/resources/json_schemas/default.json create mode 100644 jackson3/src/main/resources/json_schemas/entity_field_structure_schema.json create mode 100644 jackson3/src/main/resources/json_schemas/entity_schema.json create mode 100644 jackson3/src/test/java/edu/kit/datamanager/ro_crate/json/jackson3/Jackson3BackendParityTest.java 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/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/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" + ); + } +} From ae92bf07dea74bf3b7c4321d29c19bae1a3ad8df Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 19 Oct 2026 11:17:29 +0200 Subject: [PATCH 10/21] cleanup: remove unused imports left over from the neutral-model port --- .../edu/kit/datamanager/ro_crate/Crate.java | 35 +- .../edu/kit/datamanager/ro_crate/RoCrate.java | 1 - .../context/CrateMetadataContext.java | 14 +- .../ro_crate/entities/AbstractEntity.java | 1 - .../entities/contextual/ActionEntity.java | 357 ++++++++-------- .../entities/contextual/ActionStatus.java | 26 +- .../entities/contextual/ActionType.java | 23 +- .../entities/contextual/ContextualEntity.java | 12 +- .../entities/contextual/JsonDescriptor.java | 147 +++---- .../contextual/OrganizationEntity.java | 17 +- .../entities/contextual/PersonEntity.java | 11 +- .../entities/contextual/PlaceEntity.java | 10 +- .../ro_crate/entities/data/DataEntity.java | 288 ++++++------- .../ro_crate/entities/data/FileEntity.java | 11 +- .../entities/data/RootDataEntity.java | 12 +- .../entities/data/WorkflowEntity.java | 17 +- .../entities/validation/EntityValidation.java | 6 +- .../validation/EntityValidationStrategy.java | 6 +- .../dataentities/ImportFromZenodo.java | 52 ++- .../organizationprovider/RorProvider.java | 1 - .../personprovider/OrcidProvider.java | 2 - .../hierarchy/HierarchyRecognition.java | 369 ++++++++-------- .../hierarchy/HierarchyRecognitionConfig.java | 70 +-- .../hierarchy/HierarchyRecognitionResult.java | 81 ++-- .../ro_crate/hierarchy/package-info.java | 5 +- .../ro_crate/payload/CratePayload.java | 7 +- .../ro_crate/payload/EntityObserver.java | 4 - .../ro_crate/payload/Observer.java | 4 - .../ro_crate/payload/RoCratePayload.java | 2 - .../ro_crate/preview/AutomaticPreview.java | 126 +++--- .../ro_crate/preview/CratePreview.java | 166 ++++---- .../ro_crate/preview/CustomPreview.java | 3 - .../ro_crate/preview/CustomPreviewModel.java | 168 ++++---- .../ro_crate/preview/PreviewGenerator.java | 133 +++--- .../ro_crate/preview/StaticPreview.java | 105 +++-- .../ro_crate/reader/CrateReader.java | 1 - .../ro_crate/reader/FolderReader.java | 4 - .../reader/GenericReaderStrategy.java | 35 +- .../ro_crate/reader/ReadFolderStrategy.java | 2 - .../ro_crate/reader/ReadZipStrategy.java | 2 - .../reader/ReadZipStreamStrategy.java | 2 - .../ro_crate/reader/ReaderStrategy.java | 4 - .../datamanager/ro_crate/reader/Readers.java | 134 +++--- .../ro_crate/reader/RoCrateReader.java | 11 +- .../ro_crate/reader/ZipReader.java | 4 - .../ro_crate/special/CrateVersion.java | 170 ++++---- .../ro_crate/special/IdentifierUtils.java | 251 ++++++----- .../ClasspathPropertiesVersionProvider.java | 69 +-- .../ro_crate/util/FileSystemUtil.java | 206 ++++----- .../ro_crate/util/VersionProvider.java | 16 +- .../ro_crate/util/ZipStreamUtil.java | 149 +++---- .../ro_crate/validation/Validator.java | 4 - .../validation/ValidatorStrategy.java | 4 - .../ro_crate/writer/CrateWriter.java | 4 - .../ro_crate/writer/ElnFormatWriter.java | 38 +- .../ro_crate/writer/FolderWriter.java | 4 - .../writer/GenericWriterStrategy.java | 19 +- .../ro_crate/writer/ProvenanceManager.java | 265 ++++++------ .../ro_crate/writer/RoCrateWriter.java | 10 +- .../ro_crate/writer/WriteFolderStrategy.java | 135 +++--- .../ro_crate/writer/WriteZipStrategy.java | 41 +- .../writer/WriteZipStreamStrategy.java | 402 ++++++++++-------- .../ro_crate/writer/WriterStrategy.java | 4 - .../datamanager/ro_crate/writer/Writers.java | 60 ++- .../ro_crate/writer/ZipWriter.java | 4 - .../contextual/ContextualEntityTest.java | 2 - .../entities/data/DataSetEntityTest.java | 189 ++++---- .../entities/data/RootDataEntityTest.java | 110 ++--- .../externalproviders/RorProviderTest.java | 66 +-- .../json/jackson3/Jackson3JsonAdapter.java | 34 +- 70 files changed, 2368 insertions(+), 2379 deletions(-) diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java b/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java index 49897133..9e90b5ef 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/Crate.java @@ -1,25 +1,20 @@ package edu.kit.datamanager.ro_crate; -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.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. @@ -28,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. @@ -57,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. */ @@ -71,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. */ @@ -147,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); @@ -183,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 @@ -194,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 index 2f37e127..f68e86a0 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -13,7 +13,6 @@ 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.JsonArray; import edu.kit.datamanager.ro_crate.json.JsonObject; import edu.kit.datamanager.ro_crate.json.JsonValue; import edu.kit.datamanager.ro_crate.payload.CratePayload; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java b/core/src/main/java/edu/kit/datamanager/ro_crate/context/CrateMetadataContext.java index 23faee80..658db7ca 100644 --- a/core/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,12 +1,7 @@ package edu.kit.datamanager.ro_crate.context; -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.entities.AbstractEntity; - +import edu.kit.datamanager.ro_crate.json.JsonObject; import java.util.Map; import java.util.Set; @@ -19,7 +14,6 @@ * @version 1 */ public interface CrateMetadataContext { - JsonObject getContextJsonEntity(); boolean checkEntity(AbstractEntity entity); @@ -42,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/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 index cbb6e74c..36a40dfc 100644 --- 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 @@ -1,6 +1,5 @@ package edu.kit.datamanager.ro_crate.entities; -import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; 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; 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 index e8d644c3..c803b14a 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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.Collection; import java.util.HashSet; @@ -10,186 +6,191 @@ /** * 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); - } + 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 index 8a1c7a06..73cb5e7e 100644 --- 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 @@ -1,22 +1,18 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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; 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"); + 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; + private final String id; - ActionStatus(String id) { - this.id = id; - } + ActionStatus(String id) { + this.id = id; + } - public String getId() { - return 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 index e84a48ef..45ed2aa8 100644 --- 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 @@ -1,25 +1,20 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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; /** * Enumeration class representing action types. * @author sabrinechelbi */ public enum ActionType { + CREATE("CreateAction"), + UPDATE("UpdateAction"); - CREATE("CreateAction"), - UPDATE("UpdateAction"); + private final String name; - private final String name; + ActionType(String name) { + this.name = name; + } - ActionType(String name) { - this.name = name; - } - - public String getName(){ - return name; - } + public String getName() { + return name; + } } diff --git a/core/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 index 1165b045..c0d699d7 100644 --- a/core/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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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.entities.AbstractEntity; @@ -18,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(); @@ -29,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 index f654d868..fec0b4e7 100644 --- 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 @@ -1,95 +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.json.JsonObject; -import edu.kit.datamanager.ro_crate.json.JsonValue; - +import edu.kit.datamanager.ro_crate.special.CrateVersion; import java.net.URI; import java.util.HashSet; import java.util.Set; - -import edu.kit.datamanager.ro_crate.Crate; -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"; + 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)); - } + /** + * 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", "./"); - } + protected static ContextualEntityBuilder staticPropertiesPrefilledBuilder() { + return new ContextualEntity.ContextualEntityBuilder() + .setId(ID) + .addType("CreativeWork") + .addIdProperty("about", "./"); + } - private JsonDescriptor(ContextualEntityBuilder builder) { - super(builder); - } + 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<>(); + /** + * Builder for the JsonDescriptor. + *

+ * Defaults to the latest stable crate version and no other conformsTo values. + */ + public static final class Builder { - public Builder() { - // default - } + CrateVersion version = CrateVersion.LATEST_STABLE; + Set otherConformsToValues = new HashSet<>(); - public Builder(Crate crate) { - crate.getVersion().ifPresent(v -> this.version = v); - this.otherConformsToValues.addAll(crate.getProfiles()); - } + public Builder() { + // default + } - public Builder setVersion(CrateVersion version) { - this.version = version; - return this; - } + public Builder(Crate crate) { + crate.getVersion().ifPresent(v -> this.version = v); + this.otherConformsToValues.addAll(crate.getProfiles()); + } - public Builder addConformsTo(String uri) { - this.otherConformsToValues.add(uri); - return this; - } + public Builder setVersion(CrateVersion version) { + this.version = version; + return this; + } - public Builder addConformsTo(URI uri) { - this.otherConformsToValues.add(uri.toString()); - 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); - } + 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/core/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 index 9e13aafe..42a36e02 100644 --- a/core/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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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; /** * A helping class for creating organization entities. @@ -19,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); @@ -28,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; @@ -64,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/core/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 index a72af132..e211e57b 100644 --- a/core/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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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; /** * A helping class for creating a Person Entity. @@ -29,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; @@ -72,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/core/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 index 03ece75e..fc02ccd4 100644 --- a/core/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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.entities.contextual; -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; /** * A helping class for the Place entity creation. @@ -25,8 +21,7 @@ public PlaceEntity(AbstractPlaceEntityBuilder entityBuilder) { this.addType(TYPE); } - abstract static class AbstractPlaceEntityBuilder> extends - AbstractContextualEntityBuilder { + abstract static class AbstractPlaceEntityBuilder> extends AbstractContextualEntityBuilder { String geo; @@ -48,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 index 39e2da46..520fd982 100644 --- 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 @@ -1,14 +1,9 @@ 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 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 static edu.kit.datamanager.ro_crate.special.IdentifierUtils.isUrl; - import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; @@ -22,167 +17,174 @@ */ public class DataEntity extends AbstractEntity { - private Path path; + 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<>(); /** - * The constructor that takes an DataEntity builder and instantiates the - * entity. + * 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 entityBuilder the builder passed as argument. + * @param path the location of the data. May be null, in which case + * nothing happens. + * @return this builder */ - public DataEntity(AbstractDataEntityBuilder entityBuilder) { - super(entityBuilder); - if (!entityBuilder.authors.isEmpty()) { - this.addIdListProperties("author", entityBuilder.authors); + public T setLocation(Path path) { + if (path != null) { + if (this.getId() == null) { + this.setId(path.getFileName().toString()); } - this.path = entityBuilder.location; + this.location = path; + } + return self(); } /** - * Adds an author ID to the entity. - *

- * Calling this multiple times will add multiple author IDs. + * A variant of {@link #setLocation(Path)} which may throw exceptions. * - * @param id the identifier of the author. + * @param path the location of the data + * @return this builder + * @throws IllegalArgumentException if path is null */ - @SuppressWarnings("unused") - public void addAuthorId(String id) { - this.addIdProperty("author", id); + public T setLocationWithExceptions(Path path) + throws IllegalArgumentException { + if (path == null) { + throw new IllegalArgumentException( + "The given path should not be null." + ); + } + return setLocation(path); } /** - * Gets the path of this entity. + * Same as {@link #setLocation(Path)} but instead of associating this + * entity with a file, it will point to some place on the internet. *

- * 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. + * Via the specification, this means the uri will be set as the ID. This + * call is therefore equivalent to {@link #setId(String)}. * - * @return the path to the file this entity represents. + * @param uri the URI, should point at the data reachable on the + * internet. + * @return this builder */ - public Path getPath() { - return path; + public T setLocation(URI uri) { + if (uri != null && this.getId() == null) { + this.setId(uri.toString()); + } + return self(); } - 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(); - } + /** + * 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 addAuthor(String id) { - this.authors.add(id); - return self(); - } + public T setLicense(String id) { + this.addIdProperty("license", id); + return self(); + } - public T setContentLocation(String id) { - this.addIdProperty("contentLocation", id); - return self(); - } + public T setLicense(ContextualEntity license) { + this.addIdProperty("license", license.getId()); + return self(); + } - @Override - public abstract DataEntity build(); + public T addAuthor(String id) { + this.authors.add(id); + return self(); } - /** - * 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 { + public T setContentLocation(String id) { + this.addIdProperty("contentLocation", id); + return self(); + } - @Override - public DataEntityBuilder self() { - return this; - } + @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); - } + @Override + public DataEntity build() { + return new DataEntity(this); } + } } diff --git a/core/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 index 0b4d4f10..d5eda533 100644 --- a/core/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 @@ -1,8 +1,4 @@ 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; /** * A helping class representing a File entity. @@ -19,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); @@ -34,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/core/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 index c75e4433..cc2e75e2 100644 --- a/core/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 @@ -1,8 +1,4 @@ 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.nio.file.Paths; @@ -24,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() { @@ -34,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/core/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 index 69ab78eb..1d3f9a3b 100644 --- a/core/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 @@ -1,8 +1,4 @@ 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.ArrayList; import java.util.List; @@ -13,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. @@ -27,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<>(); @@ -50,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/core/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 index b5193f3e..c966fb04 100644 --- a/core/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,10 +1,6 @@ package edu.kit.datamanager.ro_crate.entities.validation; -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.json.JsonValue; /** * Class used for validation of entities before their creation. diff --git a/core/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 index 8dc71c2a..fd7dce9a 100644 --- a/core/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,15 +1,11 @@ package edu.kit.datamanager.ro_crate.entities.validation; -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.json.JsonValue; /** * Interface for the strategy of the entity validation. */ public interface EntityValidationStrategy { - boolean validateEntity(JsonValue entity); boolean validateFieldOfEntity(JsonValue field); diff --git a/core/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 index ba69e8e7..eb54569f 100644 --- a/core/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 @@ -1,24 +1,19 @@ package edu.kit.datamanager.ro_crate.externalproviders.dataentities; -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.apicatalog.jsonld.JsonLd; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.document.JsonDocument; - 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.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; @@ -46,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); @@ -72,7 +67,9 @@ private static void addToCrateFromZotero(String url, Crate crate) { 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"); JsonObject jsonNode; String mainId; @@ -84,9 +81,13 @@ private static void addToCrateFromZotero(String url, Crate crate) { 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); JsonObject doc = (JsonObject) Json.parse(schema.getEntity().getContent()); @@ -95,10 +96,13 @@ private static void addToCrateFromZotero(String url, Crate crate) { 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(); - + JsonDocument.of(el), + JsonDocument.of( + new ByteArrayInputStream( + doc.toString().getBytes(StandardCharsets.UTF_8) + ) + ) + ).get(); jsonNode = (JsonObject) Json.parse(finalVersion.toString()); } catch (IOException | JsonLdError e) { @@ -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((JsonObject) 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((JsonObject) 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 index fb436773..910c3d10 100644 --- 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 @@ -2,7 +2,6 @@ import edu.kit.datamanager.ro_crate.entities.contextual.OrganizationEntity; 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; diff --git a/core/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 index 43d289c0..0b3da65b 100644 --- a/core/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 @@ -3,9 +3,7 @@ import edu.kit.datamanager.ro_crate.entities.contextual.PersonEntity; 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.validation.Validation; import java.io.IOException; import org.apache.http.HttpHeaders; 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 index 6fbd2c80..5552972c 100644 --- 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 @@ -1,212 +1,203 @@ package edu.kit.datamanager.ro_crate.hierarchy; -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.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; + 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; + } + + /** + * 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; + } } - - /** - * 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); - } + 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(); + // 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); - } + 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 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(); - } - } + } + + 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 index c872e539..28eaa57c 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.hierarchy; -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; /** * Configuration class for automatic hierarchy recognition. @@ -15,33 +11,49 @@ * */ public record HierarchyRecognitionConfig( - boolean createMissingIntermediateEntities, - boolean createInverseRelationships, - boolean removeExistingConnections + boolean createMissingIntermediateEntities, + boolean createInverseRelationships, + boolean removeExistingConnections ) { - /** - * Creates a new configuration with default values. - *

- * Default values: - *

- */ - public HierarchyRecognitionConfig() { - this(false, false, false); - } + /** + * 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 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 withSetInverseRelationships(boolean value) { + return new HierarchyRecognitionConfig( + this.createMissingIntermediateEntities, + value, + this.removeExistingConnections + ); + } - public HierarchyRecognitionConfig withRemoveExistingConnections(boolean value) { - return new HierarchyRecognitionConfig(this.createMissingIntermediateEntities, this.createInverseRelationships, value); - } + 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 index bb6c7113..855cdbab 100644 --- 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 @@ -1,12 +1,7 @@ package edu.kit.datamanager.ro_crate.hierarchy; -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.entities.data.DataEntity; import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; - import java.util.*; /** @@ -16,40 +11,46 @@ * operation result, including success/failure and any errors encountered. */ public record HierarchyRecognitionResult( - Set createdEntities, - Map> processedRelationships, - Set skippedEntities, - List warnings, - List errors + 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); - } + 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/core/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 index b5692cc7..6758294e 100644 --- a/core/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,7 +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;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; +package edu.kit.datamanager.ro_crate.hierarchy; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/CratePayload.java index adaf9826..b9b7feca 100644 --- a/core/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,14 +1,9 @@ package edu.kit.datamanager.ro_crate.payload; -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.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; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java index e8c99a72..3119cbe4 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/EntityObserver.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.payload; -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; /** * Implementation of the Observer pattern, used by the deletion of entities. diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java index dbffa24f..9b1f1758 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/Observer.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.payload; -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; /** * Interface for the observer pattern used with the deletion of entities. diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java index 7460619c..5db63a61 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/payload/RoCratePayload.java @@ -5,8 +5,6 @@ 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.special.JsonUtilFunctions; import java.util.*; import java.util.stream.Collectors; 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 index 83c5feb8..bb0db62d 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.preview; -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.util.ZipStreamUtil; import java.io.File; @@ -23,71 +19,81 @@ */ public class AutomaticPreview implements CratePreview { - public AutomaticPreview() { - } + public AutomaticPreview() {} - @Override - public void saveAllToZip(ZipFile zipFile) throws IOException { + @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()) { - // 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); + 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."); - } + @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); - } + if (PreviewGenerator.isRochtmlAvailable()) { + PreviewGenerator.generatePreview(folder.getAbsolutePath()); + } else { + new CustomPreview().saveAllToFolder(folder); } + } - @Override - public void saveAllToStream(String metadata, ZipOutputStream stream) throws IOException { + @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()) { - 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); + 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 index 303aa30e..4983fdda 100644 --- 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 @@ -1,16 +1,11 @@ package edu.kit.datamanager.ro_crate.preview; -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.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 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; @@ -25,82 +20,89 @@ * @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); - } - }); - } + /** + * 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; + /** + * 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 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; + /** + * 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 index aa996f79..61c4c67c 100644 --- 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 @@ -1,8 +1,6 @@ package edu.kit.datamanager.ro_crate.preview; 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.util.ZipStreamUtil; import freemarker.template.Configuration; @@ -16,7 +14,6 @@ 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; 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 index 60f04d70..380251dc 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.preview; -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.List; @@ -12,119 +8,115 @@ */ public class CustomPreviewModel { - protected ROCrate crate; - protected List datasets; - protected List files; + 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 ROCrate getCrate() { + return crate; + } - public static class ROCrate { + public List getDatasets() { + return datasets; + } - protected String name; - protected String description; - protected String type; - protected String license; - protected String datePublished; - protected List hasPart; + public List getFiles() { + return files; + } - public String getName() { - return name; - } + public static class ROCrate { - public String getDescription() { - return description; - } + protected String name; + protected String description; + protected String type; + protected String license; + protected String datePublished; + protected List hasPart; - public String getType() { - return type; - } - - public String getLicense() { - return license; - } + public String getName() { + return name; + } - public String getDatePublished() { - return datePublished; - } + public String getDescription() { + return description; + } - public List getHasPart() { - return hasPart; - } + public String getType() { + return type; + } + public String getLicense() { + return license; } - public static class Part { + public String getDatePublished() { + return datePublished; + } - protected String id; - protected String name; + public List getHasPart() { + return hasPart; + } + } - public String getId() { - return id; - } + public static class Part { - public String getName() { - return name; - } + protected String id; + protected String name; + public String getId() { + return id; } - public static class Dataset { - - protected String id; - protected String name; - protected String description; + public String getName() { + return name; + } + } - public String getId() { - return id; - } + public static class Dataset { - public String getName() { - return name; - } + protected String id; + protected String name; + protected String description; - public String getDescription() { - return description; - } + public String getId() { + return id; + } + public String getName() { + return name; } - public static class File { + public String getDescription() { + return description; + } + } - protected String id; - protected String name; - protected String description; - protected String contentSize; - protected String encodingFormat; + public static class File { - public String getId() { - return id; - } + protected String id; + protected String name; + protected String description; + protected String contentSize; + protected String encodingFormat; - public String getName() { - return name; - } + public String getId() { + return id; + } - public String getDescription() { - return description; - } + public String getName() { + return name; + } - public String getContentSize() { - return contentSize; - } + public String getDescription() { + return description; + } - public String getEncodingFormat() { - return encodingFormat; - } + 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 index 6c1497c8..2a729674 100644 --- 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 @@ -1,13 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; -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.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,71 +14,79 @@ */ 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); + private static Logger LOG = LoggerFactory.getLogger(PreviewGenerator.class); + private static final String command = "rochtml"; - } 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; + 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"); - } + /** + * 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(); + Process process; + try { + process = builder.start(); + StringBuilder output = new StringBuilder(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream())); + 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()); - } + 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 index 41506c7c..c4ef2b99 100644 --- 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 @@ -1,13 +1,8 @@ package edu.kit.datamanager.ro_crate.preview; -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.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; @@ -23,54 +18,74 @@ */ public class StaticPreview implements CratePreview { - private final File metadataHtml; - private final File otherFiles; + private final File metadataHtml; + private final File otherFiles; - public StaticPreview(File metadataHtml, File otherFiles) { - this.metadataHtml = metadataHtml; - this.otherFiles = otherFiles; - } + public StaticPreview(File metadataHtml, File otherFiles) { + this.metadataHtml = metadataHtml; + this.otherFiles = otherFiles; + } + + public StaticPreview(File metadataHtml) { + this.metadataHtml = metadataHtml; + this.otherFiles = null; + } - 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); } - @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/" + ); + } + } - 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." + ); } - @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); - } + 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"); - } + @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 index 1d4ae61f..e230e187 100644 --- 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 @@ -7,7 +7,6 @@ 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.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; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java index d81df5d7..c6268ac1 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/FolderReader.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.reader; -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; /** * A class for reading a crate from a folder. 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 index 80465bfe..d51d6c95 100644 --- 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 @@ -1,9 +1,6 @@ package edu.kit.datamanager.ro_crate.reader; -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.json.JsonObject; import java.io.File; import java.io.IOException; @@ -14,19 +11,19 @@ * @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 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; -} \ No newline at end of file + /** + * 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 index 59170ded..1ceaae48 100644 --- 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 @@ -1,9 +1,7 @@ package edu.kit.datamanager.ro_crate.reader; 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.File; import java.io.IOException; import java.nio.file.Path; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java index ca40bdb1..94e1711a 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.java @@ -2,9 +2,7 @@ import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; 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.util.FileSystemUtil; import java.io.File; import java.io.IOException; 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 index 363355da..26a748ff 100644 --- 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 @@ -2,9 +2,7 @@ import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor; 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.util.FileSystemUtil; import java.io.File; import java.io.FileOutputStream; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java index 905bf8f2..2029fe9c 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ReaderStrategy.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.reader; -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; /** * Interface for the strategy for the reader class. 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 index e14396c9..8dc6436e 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.reader; -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.InputStream; import java.nio.file.Path; @@ -13,69 +9,79 @@ */ public class Readers { - /** - * Private constructor to prevent instantiation of this utility class. - */ - private 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. + * + * @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 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 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. + * + * @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)); - } + /** + * 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/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java index 46e8a708..9156b9ba 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/RoCrateReader.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.reader; -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; /** * This class allows reading crates from the outside into the library in order @@ -18,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/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java index d380258f..d92faf94 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipReader.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.reader; -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.nio.file.Path; 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 index fd0f0cfb..4e7bf749 100644 --- 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 @@ -1,8 +1,4 @@ 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 java.net.URI; import java.net.URISyntaxException; @@ -10,106 +6,106 @@ /** * 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: 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; + // 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 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; + /** + * 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)); - } + /** + * 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); - } + /** + * 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 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; - } + 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]; - } + /** + * 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; + 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 isGreaterThan(CrateVersion other) { + return this.compareTo(other) > 0; + } - public boolean isGreaterOrEqualThan(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 isLowerThan(CrateVersion other) { + return this.compareTo(other) < 0; + } - public boolean isLowerOrEqualThan(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 index 12a792e7..d8cebdfc 100644 --- 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 @@ -1,10 +1,9 @@ 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.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; @@ -13,10 +12,6 @@ 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. @@ -26,137 +21,141 @@ */ public class IdentifierUtils { - /** Static class */ - private 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 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; - } + /** + * 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(); + /** + * 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(); - } + /** + * 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(); + /** + * 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. + /** + * 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(); - } + 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 - public static Optional decode(String uri) { - if (!isValidUri(uri) || isLdBlankNode(uri)) { - return Optional.of(uri); - } - return Optional.of(URLDecoder.decode(uri, StandardCharsets.UTF_8)); + if (isValidUri(result)) { + return Optional.of(result); + } else { + return Optional.empty(); } + } - /** - * 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(" "); + 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/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 index 646d7b8a..8431ee44 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.util; -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.io.InputStream; @@ -10,37 +6,42 @@ 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 + + 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; } - @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); - } + 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 index fcc4d1c4..2a2a86f8 100644 --- 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 @@ -1,126 +1,128 @@ package edu.kit.datamanager.ro_crate.util; -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 org.apache.commons.io.FileUtils; 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://") - ); - } + private FileSystemUtil() { + // Utility class, no instantiation + } - /** - * 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; - } + /** + * 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://") + ) + ); + } - // Normalize path - remove trailing slash for consistency - String normalizedPath = path.endsWith("/") - ? path.substring(0, path.length() - 1) - : path; + /** + * 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; + } - int lastSlash = normalizedPath.lastIndexOf('/'); - if (lastSlash == -1) { - return "./"; // Root directory - } + // Normalize path - remove trailing slash for consistency + String normalizedPath = path.endsWith("/") + ? path.substring(0, path.length() - 1) + : path; - String parentPath = normalizedPath.substring(0, lastSlash); + int lastSlash = normalizedPath.lastIndexOf('/'); + if (lastSlash == -1) { + return "./"; // Root directory + } - // If parent is empty, it's root - if (parentPath.isEmpty()) { - return "./"; - } + String parentPath = normalizedPath.substring(0, lastSlash); - // For validation, we need to check both with and without trailing slash - // since files don't have trailing slash but folders do - return parentPath; + // If parent is empty, it's root + if (parentPath.isEmpty()) { + return "./"; } - /** - * 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; + // 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; + /** + * 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(); + /** + * 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); + if (isNonEmptyDir || isFile) { + FileUtils.forceDelete(folder); } + FileUtils.forceMkdir(folder); + } } 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 index 07b38395..e14dd222 100644 --- 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 @@ -1,14 +1,10 @@ package edu.kit.datamanager.ro_crate.util; -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; public interface VersionProvider { - /** - * Returns the version of the ro-crate-java library. - * - * @return The version string. - */ - String getVersion(); + /** + * 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 index d8125b18..e258b559 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.util; -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.File; import java.io.FileInputStream; @@ -17,81 +13,88 @@ */ 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; - } + /** + * 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() + ) + ); + } - 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); - } - } + File[] files = folder.listFiles(); + if (files == null) { + return; } - /** - * 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); + 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 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); + /** + * 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); + } - try (InputStream inputStream = new FileInputStream(file)) { - byte[] buffer = new byte[4096]; - int len; - while ((len = inputStream.read(buffer)) != -1) { - zipOutputStream.write(buffer, 0, len); - } - } + /** + * 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); - zipOutputStream.closeEntry(); + 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/Validator.java b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java index 8cd05721..d408b97e 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/Validator.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.validation; -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.Crate; diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java index c5c31057..94e68906 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/validation/ValidatorStrategy.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.validation; -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.Crate; 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 index 43bb41d7..3aa67f7c 100644 --- 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 @@ -1,10 +1,6 @@ package edu.kit.datamanager.ro_crate.writer; 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.json.JsonObject; -import edu.kit.datamanager.ro_crate.json.JsonValue; import edu.kit.datamanager.ro_crate.validation.Validation; import edu.kit.datamanager.ro_crate.validation.Validator; import java.io.IOException; 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 index 09bd1074..bb767d1e 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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; /** * An Interface for {@link GenericWriterStrategy} implementations which support writing @@ -10,22 +6,22 @@ * * @param the type which determines the destination of the result */ -public interface ElnFormatWriter extends GenericWriterStrategy { +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(); - /** - * 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(); - } + /** + * Alias with more generic name for {@link #usingElnStyle()}. + * + * @return this writer + */ + default ElnFormatWriter withRootSubdirectory() { + return this.usingElnStyle(); + } } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java index 304f6b79..1426414e 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/FolderWriter.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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; /** * A class for writing a crate to a folder. diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/GenericWriterStrategy.java index 3ead88f9..b0bed7bb 100644 --- a/core/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,11 +1,6 @@ package edu.kit.datamanager.ro_crate.writer; -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.Crate; - import java.io.IOException; /** @@ -15,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 index 2c00040a..d6ef92bf 100644 --- 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 @@ -1,142 +1,165 @@ package edu.kit.datamanager.ro_crate.writer; -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 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; -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()); - } + /** + * 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 ProvenanceManager with a specified VersionProvider. + * Constructs a String with the given suffix. * - * @param versionProvider The VersionProvider to use for retrieving the version of ro-crate-java. + * @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 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(); + public String withSuffix(String suffix) { + return prefix + "-" + suffix; } - 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; + @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/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java index 4a2f5b5a..92d34af9 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/RoCrateWriter.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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; /** * The class used for writing (exporting) crates. The class uses a strategy @@ -13,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 index e3b446ef..a15c6627 100644 --- 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 @@ -1,22 +1,19 @@ package edu.kit.datamanager.ro_crate.writer; -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.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 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; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A class for writing a crate to a folder. @@ -26,66 +23,78 @@ */ public class WriteFolderStrategy implements GenericWriterStrategy { - private static final Logger logger = LoggerFactory.getLogger(WriteFolderStrategy.class); + private static final Logger logger = LoggerFactory.getLogger( + WriteFolderStrategy.class + ); - protected boolean writePreview = true; + 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; - } + /** + * 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)); + @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); - } + 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()); - } - } + 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 index 0dccbc5e..ed336e93 100644 --- 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 @@ -1,37 +1,34 @@ package edu.kit.datamanager.ro_crate.writer; -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.Crate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - 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 +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; - } + 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)); - } + @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 index 546aa57d..e3dcceb9 100644 --- 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 @@ -1,13 +1,13 @@ package edu.kit.datamanager.ro_crate.writer; -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.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; @@ -16,11 +16,6 @@ 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; @@ -31,193 +26,222 @@ * 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; +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); } - - /** - * 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; + try (ZipOutputStream zipFile = new ZipOutputStream(destination)) { + saveMetadataJson(crate, zipFile, innerFolderName); + saveDataEntities(crate, zipFile, innerFolderName); + savePreview(crate, zipFile, innerFolderName); } - - @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 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); + } } - - 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(); + zipStream.closeEntry(); + } + + private void savePreview( + Crate crate, + ZipOutputStream zipStream, + String prefix + ) throws IOException { + Optional preview = Optional.ofNullable(crate.getPreview()); + if (preview.isEmpty()) { + return; } - - 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); - } + 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." + ); } - - 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); - } + 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; } - /** - * 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 ""; - } + 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); - } + // 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 ""; } - return String.join("/", stack); + stack.remove(stack.size() - 1); + } else { + stack.add(segment); + } } + return String.join("/", stack); + } } diff --git a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java index e94a9a60..12459673 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/WriterStrategy.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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; /** * Strategy for writing of crates. 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 index eb011346..729403c2 100644 --- 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 @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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.OutputStream; @@ -12,35 +8,35 @@ */ public class Writers { - /** - * Prevents instantiation of this utility class. - */ - private 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 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 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()); - } + /** + * 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/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java index 5e8cfd14..c9c0735e 100644 --- a/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java +++ b/core/src/main/java/edu/kit/datamanager/ro_crate/writer/ZipWriter.java @@ -1,8 +1,4 @@ package edu.kit.datamanager.ro_crate.writer; -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; /** * Implementation of the writing strategy to provide a way of writing crates to 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 index c9e4baf0..c990de6f 100644 --- 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 @@ -4,9 +4,7 @@ import edu.kit.datamanager.ro_crate.HelpFunctions; 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 org.junit.jupiter.api.Test; 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 index 92d81972..b0fb17eb 100644 --- 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 @@ -1,104 +1,115 @@ 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.io.IOException; +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 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() { - 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); - } + @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/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 index ffd0f789..a16aae6c 100644 --- 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 @@ -1,16 +1,11 @@ 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 static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; - 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; /** @@ -19,49 +14,68 @@ */ 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(); + @Test + void testSerialization() throws IOException { + String id1 = "file1_id"; + FileEntity file1 = new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(Paths.get("does_not_matter")) + .setId(id1) + .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(); + String id2 = "file2_id"; + FileEntity file2 = new FileEntity.FileEntityBuilder() + .addIdProperty("name", "dsklfajs") + .addIdProperty("name", "11111111") + .setLocationWithExceptions(Paths.get("does_not_matter")) + .setId(id2) + .build(); - assertTrue(rootDataEntity.hasPart(id1)); - assertTrue(rootDataEntity.hasPart(id2)); - HelpFunctions.compareEntityWithFile(rootDataEntity, "/json/entities/data/root.json"); - } + 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(); - @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"); + 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/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 index 9300e157..f26861e0 100644 --- 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 @@ -1,43 +1,53 @@ package edu.kit.datamanager.ro_crate.externalproviders; -import java.io.IOException; +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.HelpFunctions; 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; -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); - } + @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/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 index 695091eb..4ce22654 100644 --- 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 @@ -1,10 +1,5 @@ package edu.kit.datamanager.ro_crate.json.jackson3; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.node.ArrayNode; -import tools.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; @@ -19,6 +14,10 @@ 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. @@ -35,9 +34,13 @@ 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(); + .enable( + tools.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY + ) + .enable( + tools.jackson.databind.DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS + ) + .build(); @Override public JsonObject objectNode() { @@ -71,7 +74,9 @@ public String write(JsonValue value) { @Override public String writePretty(JsonValue value) { - return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(toJackson(value)); + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + toJackson(value) + ); } /** Converts a Jackson 3 tree into the neutral model. */ @@ -106,7 +111,8 @@ public static JsonValue toNeutral(JsonNode node) { return new JsonNumber(numberOf(node)); } throw new IllegalArgumentException( - "Unsupported Jackson node type: " + node.getNodeType()); + "Unsupported Jackson node type: " + node.getNodeType() + ); } private static Number numberOf(JsonNode node) { @@ -154,8 +160,9 @@ public static JsonNode toJackson(JsonValue value) { return MAPPER.getNodeFactory().textNode(string.getValue()); } if (value instanceof JsonBoolean bool) { - return bool.getValue() ? MAPPER.getNodeFactory().booleanNode(true) - : MAPPER.getNodeFactory().booleanNode(false); + return bool.getValue() + ? MAPPER.getNodeFactory().booleanNode(true) + : MAPPER.getNodeFactory().booleanNode(false); } if (value instanceof JsonNull) { return MAPPER.getNodeFactory().nullNode(); @@ -167,7 +174,8 @@ public static JsonNode toJackson(JsonValue value) { return numberNode(number.getValue()); } throw new IllegalArgumentException( - "Unsupported neutral JSON value: " + value.getClass()); + "Unsupported neutral JSON value: " + value.getClass() + ); } private static JsonNode numberNode(Number number) { From 1a4fbe9fed7614f4038d985c0479c3bf32f60b46 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 19 Oct 2026 11:57:31 +0200 Subject: [PATCH 11/21] docs: document core/jackson2/jackson3 module layout and Jackson 3 usage --- README.md | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6d896722..e8021ed3 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,54 @@ and avoiding crates which do not fully comply to the specification, at the same - [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. + ## 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`. ## 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 +87,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. From 1d242f8f23ed5db521c97c2cbaeebdf8fd755540 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 10:05:49 +0200 Subject: [PATCH 12/21] docs: add migration guide for the Jackson-free core --- MIGRATION.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 197 insertions(+) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..980f3542 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,196 @@ +# 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. + +## Providing your own validator + +Validation behaves like the JSON backends: the core defines the interfaces +`edu.kit.datamanager.ro_crate.validation.ValidatorStrategy` and +`edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy`, +the backend modules implement them, and the implementations are listed in a +`META-INF/services` file and loaded with `ServiceLoader`. The JSON-schema +implementation keeps its historical fully qualified name in the `jackson2` +module (`edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation`), with +neutral JSON types in its signatures. + +To provide your own validator, implement the interface and list your class in +a file named +`META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy` +inside your jar. + +## 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 e8021ed3..b964ebf9 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ 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) From 3e328ca6890d3a31fc5afedb826818f0728f4933 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 10:22:27 +0200 Subject: [PATCH 13/21] docs: describe when validation runs and how to provide custom validators --- MIGRATION.md | 79 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 980f3542..b8315d21 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -169,21 +169,70 @@ 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. -## Providing your own validator - -Validation behaves like the JSON backends: the core defines the interfaces -`edu.kit.datamanager.ro_crate.validation.ValidatorStrategy` and -`edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy`, -the backend modules implement them, and the implementations are listed in a -`META-INF/services` file and loaded with `ServiceLoader`. The JSON-schema -implementation keeps its historical fully qualified name in the `jackson2` -module (`edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation`), with -neutral JSON types in its signatures. - -To provide your own validator, implement the interface and list your class in -a file named -`META-INF/services/edu.kit.datamanager.ro_crate.validation.ValidatorStrategy` -inside your jar. +## 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); +``` + +Do not register your own implementation in a `META-INF/services` file: the +backend module already provides one, and the core loads exactly one +implementation per interface. A second one makes every validation fail with +an `IllegalStateException`. Your own strategy is therefore never picked up +implicitly; pass it explicitly as shown above. ## Removed classes From 6d5c0a7e0a4711154d473fff693ef963956881f4 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 10:42:47 +0200 Subject: [PATCH 14/21] docs: drop confusing guidance on registering custom validators --- MIGRATION.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index b8315d21..a7f77a3e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -228,12 +228,6 @@ Validator validator = new Validator(crate -> { validator.validate(crate); ``` -Do not register your own implementation in a `META-INF/services` file: the -backend module already provides one, and the core loads exactly one -implementation per interface. A second one makes every validation fail with -an `IllegalStateException`. Your own strategy is therefore never picked up -implicitly; pass it explicitly as shown above. - ## Removed classes Internal classes without a replacement: From 97b5a9bc7373ac47910b34739b19677e50a88331 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 12:23:50 +0200 Subject: [PATCH 15/21] test: add examples module comparing released 2.1.2 with current backends --- examples/build.gradle | 89 +++++++++++++++++++ .../java/examples/CreateMinimalCrate.java | 77 ++++++++++++++++ .../java/examples/CreateMinimalCrate.java | 80 +++++++++++++++++ .../src/test/java/examples/ParityTest.java | 41 +++++++++ settings.gradle | 2 +- 5 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 examples/build.gradle create mode 100644 examples/src/legacy/java/examples/CreateMinimalCrate.java create mode 100644 examples/src/main/java/examples/CreateMinimalCrate.java create mode 100644 examples/src/test/java/examples/ParityTest.java diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 00000000..6423d9a5 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,89 @@ +/* + * Executable examples for ro-crate-java that double as a behavior-comparison + * test suite: each example is run in its own JVM (required — the JSON + * backends register via ServiceLoader and must not share a classpath) and + * writes its canonical output to a file. The parity test then asserts that + * the output of the released 2.1.2 artifact ("legacy") is identical to the + * current code running with the jackson2 backend, which again is identical + * to the current code running with the jackson3 backend. + */ + +plugins { + id 'java' +} + +ext { + legacyVersion = '2.1.2' +} + +repositories { + mavenCentral() +} + +sourceSets { + // the legacy examples are compiled and run against the released artifact, + // which uses the pre-port Jackson API; kept fully separate from the main + // (current) source set so the two classpaths never mix + 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') +} + +// one output file per example and variant: build/examples//.txt +def variants = [ + 'legacy' : { sourceSets.legacy.runtimeClasspath }, + 'current-jackson2' : { sourceSets.main.runtimeClasspath + configurations.jackson2Backend }, + 'current-jackson3' : { sourceSets.main.runtimeClasspath + configurations.jackson3Backend }, +] + +def exampleClasses = ['CreateMinimalCrate'] + +exampleClasses.each { example -> + variants.each { variant, variantClasspath -> + tasks.register("run${variant.capitalize()}${example}", JavaExec) { + group = 'examples' + description = "Runs the ${example} example with the ${variant} variant." + dependsOn variantClasses(variant) + classpath = variantClasspath() + 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}") } + } +} + +def variantClasses(variant) { + switch (variant) { + case 'legacy': return tasks.named('legacyClasses') + default: return tasks.named('classes') + } +} diff --git a/examples/src/legacy/java/examples/CreateMinimalCrate.java b/examples/src/legacy/java/examples/CreateMinimalCrate.java new file mode 100644 index 00000000..f3dda456 --- /dev/null +++ b/examples/src/legacy/java/examples/CreateMinimalCrate.java @@ -0,0 +1,77 @@ +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.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.objectmapper.MyObjectMapper; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Legacy variant of {@code CreateMinimalCrate}, written against the + * released 2.1.2 API (Jackson types in the library). The example code is + * identical to the current variant; only the canonicalization helper 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(); + + Path output = Path.of(args[0]); + Files.writeString(output, canonicalize(crate.getJsonMetadata())); + } + + /** + * Same canonicalization as the current variant, but with the legacy + * Jackson API: entities sorted by their {@code @id}, compact JSON. + */ + 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/main/java/examples/CreateMinimalCrate.java b/examples/src/main/java/examples/CreateMinimalCrate.java new file mode 100644 index 00000000..05f1be8e --- /dev/null +++ b/examples/src/main/java/examples/CreateMinimalCrate.java @@ -0,0 +1,80 @@ +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.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.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Builds a small crate programmatically and prints its metadata in + * canonical form (entities sorted by their {@code @id}, compact JSON). + * + *

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(); + + Path output = Path.of(args[0]); + Files.writeString(output, canonicalize(crate.getJsonMetadata())); + } + + /** + * Sorts the {@code @graph} entities by their {@code @id} (the entity order + * follows Java hash iteration and is unspecified) and writes compact JSON, + * so that runs of different library versions can be compared byte-for-byte. + */ + 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/test/java/examples/ParityTest.java b/examples/src/test/java/examples/ParityTest.java new file mode 100644 index 00000000..f0917d3c --- /dev/null +++ b/examples/src/test/java/examples/ParityTest.java @@ -0,0 +1,41 @@ +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 org.junit.jupiter.api.Test; + +/** + * Compares the outputs of the example runs: the released 2.1.2 artifact + * must produce byte-identical metadata to the current code with the + * jackson2 backend, which again must match the current code with the + * jackson3 backend. The run tasks (see build.gradle) execute each example + * once per variant in its own JVM and write to build/examples/<variant>/. + */ +class ParityTest { + + private static final Path EXAMPLES_DIR = Path.of("build", "examples"); + private static final String EXAMPLE = "CreateMinimalCrate"; + + private static String output(String variant) throws IOException { + Path file = EXAMPLES_DIR.resolve(variant).resolve(EXAMPLE + ".txt"); + assertTrue( + Files.exists(file), + "missing output " + file + " — the run tasks should have produced it" + ); + return Files.readString(file); + } + + @Test + void legacyMatchesCurrentJackson2() throws IOException { + assertEquals(output("legacy"), output("current-jackson2")); + } + + @Test + void currentJackson2MatchesCurrentJackson3() throws IOException { + assertEquals(output("current-jackson2"), output("current-jackson3")); + } +} diff --git a/settings.gradle b/settings.gradle index b8a1965b..3e70723e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'ro-crate-java' -include 'core', 'jackson2', 'jackson3' +include 'core', 'jackson2', 'jackson3', 'examples' From 7b4a203b45ce78d332ddcd5922e6dd8f33b5f2bc Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 12:52:45 +0200 Subject: [PATCH 16/21] docs: document the examples module commands --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index b964ebf9..cac53855 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,25 @@ No further configuration is needed: the backend registers itself via 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](core/src/test/java/edu/kit/datamanager/ro_crate/examples/ExamplesOfSpecificationV1p1Test.java)) From b2b0d688d538cc22010bbe81aab0db472e396331 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 15:00:50 +0200 Subject: [PATCH 17/21] test: add read, modify, write and validate examples to the parity suite --- examples/build.gradle | 8 +- .../src/legacy/java/examples/ModifyCrate.java | 100 ++++++++++++ .../src/legacy/java/examples/ReadCrate.java | 70 ++++++++ .../legacy/java/examples/ValidateCrate.java | 35 ++++ .../src/legacy/java/examples/WriteCrate.java | 139 ++++++++++++++++ .../src/main/java/examples/ModifyCrate.java | 114 +++++++++++++ .../src/main/java/examples/ReadCrate.java | 71 +++++++++ .../src/main/java/examples/ValidateCrate.java | 48 ++++++ .../src/main/java/examples/WriteCrate.java | 150 ++++++++++++++++++ 9 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 examples/src/legacy/java/examples/ModifyCrate.java create mode 100644 examples/src/legacy/java/examples/ReadCrate.java create mode 100644 examples/src/legacy/java/examples/ValidateCrate.java create mode 100644 examples/src/legacy/java/examples/WriteCrate.java create mode 100644 examples/src/main/java/examples/ModifyCrate.java create mode 100644 examples/src/main/java/examples/ReadCrate.java create mode 100644 examples/src/main/java/examples/ValidateCrate.java create mode 100644 examples/src/main/java/examples/WriteCrate.java diff --git a/examples/build.gradle b/examples/build.gradle index 6423d9a5..9128aac6 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -55,7 +55,13 @@ def variants = [ 'current-jackson3' : { sourceSets.main.runtimeClasspath + configurations.jackson3Backend }, ] -def exampleClasses = ['CreateMinimalCrate'] +def exampleClasses = [ + 'CreateMinimalCrate', + 'ReadCrate', + 'ModifyCrate', + 'WriteCrate', + 'ValidateCrate', +] exampleClasses.each { example -> variants.each { variant, variantClasspath -> diff --git a/examples/src/legacy/java/examples/ModifyCrate.java b/examples/src/legacy/java/examples/ModifyCrate.java new file mode 100644 index 00000000..5c17cde7 --- /dev/null +++ b/examples/src/legacy/java/examples/ModifyCrate.java @@ -0,0 +1,100 @@ +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.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.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.reader.Readers; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Legacy variant of {@code ModifyCrate}, written against the released 2.1.2 + * API. The example code is identical to the current variant; only the + * canonicalization helper uses the Jackson API. + */ +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(canonicalize(crate.getJsonMetadata())).append("\n\n"); + + // remove the data entity by its id + crate.deleteEntityById("data.txt"); + report.append("after deleteEntityById(data.txt)\n") + .append(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 data") + .addProperty("encodingFormat", "text/plain") + .build()); + report.append("after addDataEntity(data.txt)\n") + .append(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(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(canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("https://ror.org/04jsmm180"); + report.append("after deleteEntityById(organization)\n") + .append(canonicalize(crate.getJsonMetadata())); + + Files.writeString(Path.of(args[0]), report.toString()); + } + + /** + * Same canonicalization as the current variant, but with the legacy + * Jackson API: entities sorted by their {@code @id}, compact JSON. + */ + 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/ReadCrate.java b/examples/src/legacy/java/examples/ReadCrate.java new file mode 100644 index 00000000..326984fb --- /dev/null +++ b/examples/src/legacy/java/examples/ReadCrate.java @@ -0,0 +1,70 @@ +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.RoCrate; +import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +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; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Legacy variant of {@code ReadCrate}, written against the released 2.1.2 + * API. The example code is identical to the current variant; only the + * canonicalization helper uses the Jackson API. + */ +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 zip = Path.of(args[0]).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(canonicalize(folderCrate.getJsonMetadata())) + .append("\n\nfrom zip\n") + .append(canonicalize(zipCrate.getJsonMetadata())) + .append("\n"); + Files.writeString(Path.of(args[0]), report.toString()); + } + + /** + * Same canonicalization as the current variant, but with the legacy + * Jackson API: entities sorted by their {@code @id}, compact JSON. + */ + 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/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..864e7763 --- /dev/null +++ b/examples/src/legacy/java/examples/WriteCrate.java @@ -0,0 +1,139 @@ +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.RoCrate; +import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +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.ArrayList; +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 the + * canonicalization helper uses the Jackson API. + */ +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(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; + } + + /** + * Same canonicalization as the current variant, but with the legacy + * Jackson API: entities sorted by their {@code @id}, compact JSON. + */ + 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/main/java/examples/ModifyCrate.java b/examples/src/main/java/examples/ModifyCrate.java new file mode 100644 index 00000000..8c1675db --- /dev/null +++ b/examples/src/main/java/examples/ModifyCrate.java @@ -0,0 +1,114 @@ +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.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 java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Reads a fixture crate from disk and modifies it: adds and removes entities, + * adds and removes properties. Prints the canonical metadata after each step, + * 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(canonicalize(crate.getJsonMetadata())).append("\n\n"); + + // remove the data entity by its id + crate.deleteEntityById("data.txt"); + report + .append("after deleteEntityById(data.txt)\n") + .append(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 data") + .addProperty("encodingFormat", "text/plain") + .build() + ); + report + .append("after addDataEntity(data.txt)\n") + .append(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(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(canonicalize(crate.getJsonMetadata())) + .append("\n\n"); + crate.deleteEntityById("https://ror.org/04jsmm180"); + report + .append("after deleteEntityById(organization)\n") + .append(canonicalize(crate.getJsonMetadata())); + + Files.writeString(Path.of(args[0]), report.toString()); + } + + /** + * Sorts the {@code @graph} entities by their {@code @id} (the entity order + * follows Java hash iteration and is unspecified) and writes compact JSON, + * so that runs of different library versions can be compared byte-for-byte. + */ + 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/ReadCrate.java b/examples/src/main/java/examples/ReadCrate.java new file mode 100644 index 00000000..312cdffa --- /dev/null +++ b/examples/src/main/java/examples/ReadCrate.java @@ -0,0 +1,71 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +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.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 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 prints the + * canonical metadata for both paths. + */ +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 zip = Path.of(args[0]).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(canonicalize(folderCrate.getJsonMetadata())) + .append("\n\nfrom zip\n") + .append(canonicalize(zipCrate.getJsonMetadata())) + .append("\n"); + Files.writeString(Path.of(args[0]), report.toString()); + } + + /** + * Sorts the {@code @graph} entities by their {@code @id} (the entity order + * follows Java hash iteration and is unspecified) and writes compact JSON, + * so that runs of different library versions can be compared byte-for-byte. + */ + 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/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..d3c71c90 --- /dev/null +++ b/examples/src/main/java/examples/WriteCrate.java @@ -0,0 +1,150 @@ +package examples; + +import edu.kit.datamanager.ro_crate.RoCrate; +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.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.ArrayList; +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 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(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; + } + + /** + * Sorts the {@code @graph} entities by their {@code @id} (the entity order + * follows Java hash iteration and is unspecified) and writes compact JSON, + * so that runs of different library versions can be compared byte-for-byte. + */ + 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); + } +} From b2d46361150debe542cf31629141f535a21fc3b2 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 15:43:03 +0200 Subject: [PATCH 18/21] cleanup(examples): compare all examples in the parity test and remove duplicated helpers --- examples/build.gradle | 76 +++++++++---- .../legacy/java/examples/CanonicalJson.java | 45 ++++++++ .../java/examples/CreateMinimalCrate.java | 46 ++------ .../src/legacy/java/examples/ModifyCrate.java | 103 +++++++++--------- .../src/legacy/java/examples/ReadCrate.java | 71 +++++------- .../src/legacy/java/examples/WriteCrate.java | 62 ++++------- .../src/main/java/examples/CanonicalJson.java | 46 ++++++++ .../java/examples/CreateMinimalCrate.java | 43 +------- .../src/main/java/examples/ModifyCrate.java | 71 +++++------- .../src/main/java/examples/ReadCrate.java | 71 +++++------- .../src/main/java/examples/WriteCrate.java | 40 +------ .../src/test/java/examples/ParityTest.java | 98 +++++++++++++++-- 12 files changed, 396 insertions(+), 376 deletions(-) create mode 100644 examples/src/legacy/java/examples/CanonicalJson.java create mode 100644 examples/src/main/java/examples/CanonicalJson.java diff --git a/examples/build.gradle b/examples/build.gradle index 9128aac6..7831d264 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,11 +1,30 @@ /* * Executable examples for ro-crate-java that double as a behavior-comparison - * test suite: each example is run in its own JVM (required — the JSON - * backends register via ServiceLoader and must not share a classpath) and - * writes its canonical output to a file. The parity test then asserts that - * the output of the released 2.1.2 artifact ("legacy") is identical to the - * current code running with the jackson2 backend, which again is identical - * to the current code running with the jackson3 backend. + * 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 { @@ -21,9 +40,8 @@ repositories { } sourceSets { - // the legacy examples are compiled and run against the released artifact, - // which uses the pre-port Jackson API; kept fully separate from the main - // (current) source set so the two classpaths never mix + // compiled and run against the released artifact; deliberately not + // sharing a classpath with the current code legacy } @@ -48,13 +66,30 @@ dependencies { testRuntimeOnly('org.junit.platform:junit-platform-launcher') } -// one output file per example and variant: build/examples//.txt +/* + * 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' : { sourceSets.legacy.runtimeClasspath }, - 'current-jackson2' : { sourceSets.main.runtimeClasspath + configurations.jackson2Backend }, - 'current-jackson3' : { sourceSets.main.runtimeClasspath + configurations.jackson3Backend }, + 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', @@ -64,12 +99,12 @@ def exampleClasses = [ ] exampleClasses.each { example -> - variants.each { variant, variantClasspath -> + variants.each { variant, setup -> tasks.register("run${variant.capitalize()}${example}", JavaExec) { group = 'examples' description = "Runs the ${example} example with the ${variant} variant." - dependsOn variantClasses(variant) - classpath = variantClasspath() + dependsOn setup.classesTask + classpath = setup.classpath() mainClass = "examples.${example}" def outputFile = layout.buildDirectory.file("examples/${variant}/${example}.txt") args outputFile.get().asFile.absolutePath @@ -85,11 +120,6 @@ tasks.named('test') { dependsOn exampleClasses.collectMany { example -> variants.keySet().collect { variant -> tasks.named("run${variant.capitalize()}${example}") } } -} - -def variantClasses(variant) { - switch (variant) { - case 'legacy': return tasks.named('legacyClasses') - default: return tasks.named('classes') - } + // 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 index f3dda456..682eb8f2 100644 --- a/examples/src/legacy/java/examples/CreateMinimalCrate.java +++ b/examples/src/legacy/java/examples/CreateMinimalCrate.java @@ -1,24 +1,16 @@ 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.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.objectmapper.MyObjectMapper; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** - * Legacy variant of {@code CreateMinimalCrate}, written against the - * released 2.1.2 API (Jackson types in the library). The example code is - * identical to the current variant; only the canonicalization helper uses - * the Jackson API instead of the neutral JSON model. + * 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 { @@ -47,31 +39,9 @@ public static void main(String[] args) throws Exception { ) .build(); - Path output = Path.of(args[0]); - Files.writeString(output, canonicalize(crate.getJsonMetadata())); - } - - /** - * Same canonicalization as the current variant, but with the legacy - * Jackson API: entities sorted by their {@code @id}, compact JSON. - */ - 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); + 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 index 5c17cde7..1ca59245 100644 --- a/examples/src/legacy/java/examples/ModifyCrate.java +++ b/examples/src/legacy/java/examples/ModifyCrate.java @@ -1,60 +1,77 @@ 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.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.objectmapper.MyObjectMapper; import edu.kit.datamanager.ro_crate.reader.Readers; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** * Legacy variant of {@code ModifyCrate}, written against the released 2.1.2 - * API. The example code is identical to the current variant; only the - * canonicalization helper uses the Jackson API. + * 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"); + "..", + "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(canonicalize(crate.getJsonMetadata())).append("\n\n"); + report + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); - // remove the data entity by its id + // 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(canonicalize(crate.getJsonMetadata())) - .append("\n\n"); + report + .append("after deleteEntityById(data.txt)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); - // add it back, with different properties + // add it back with different properties crate.addDataEntity( new DataEntity.DataEntityBuilder() .setId("data.txt") .addType("File") - .addProperty("name", "Some data") + .addProperty("name", "Some other data") .addProperty("encodingFormat", "text/plain") - .build()); - report.append("after addDataEntity(data.txt)\n") - .append(canonicalize(crate.getJsonMetadata())) - .append("\n\n"); + .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(canonicalize(crate.getJsonMetadata())) - .append("\n\n"); + 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 = @@ -64,37 +81,15 @@ public static void main(String[] args) throws Exception { .addProperty("name", "Karlsruhe Institute of Technology") .build(); crate.addContextualEntity(organization); - report.append("after addContextualEntity(organization)\n") - .append(canonicalize(crate.getJsonMetadata())) - .append("\n\n"); + 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(canonicalize(crate.getJsonMetadata())); + report + .append("after deleteEntityById(organization)\n") + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())); Files.writeString(Path.of(args[0]), report.toString()); } - - /** - * Same canonicalization as the current variant, but with the legacy - * Jackson API: entities sorted by their {@code @id}, compact JSON. - */ - 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/ReadCrate.java b/examples/src/legacy/java/examples/ReadCrate.java index 326984fb..d732fcd4 100644 --- a/examples/src/legacy/java/examples/ReadCrate.java +++ b/examples/src/legacy/java/examples/ReadCrate.java @@ -1,70 +1,51 @@ 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.RoCrate; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; 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; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** * Legacy variant of {@code ReadCrate}, written against the released 2.1.2 - * API. The example code is identical to the current variant; only the - * canonicalization helper uses the Jackson API. + * 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"); + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); public static void main(String[] args) throws Exception { - Path zip = Path.of(args[0]).getParent().resolve("simple_crate.zip"); - RoCrate folderCrate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + 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()); + RoCrate zipCrate = Readers.newZipPathReader().readCrate( + zip.toAbsolutePath().toString() + ); StringBuilder report = new StringBuilder(); - report.append("from folder\n") - .append(canonicalize(folderCrate.getJsonMetadata())) - .append("\n\nfrom zip\n") - .append(canonicalize(zipCrate.getJsonMetadata())) - .append("\n"); - Files.writeString(Path.of(args[0]), report.toString()); - } - - /** - * Same canonicalization as the current variant, but with the legacy - * Jackson API: entities sorted by their {@code @id}, compact JSON. - */ - 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); + 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/WriteCrate.java b/examples/src/legacy/java/examples/WriteCrate.java index 864e7763..7d078635 100644 --- a/examples/src/legacy/java/examples/WriteCrate.java +++ b/examples/src/legacy/java/examples/WriteCrate.java @@ -1,18 +1,12 @@ 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.RoCrate; -import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; 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.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Stream; @@ -21,13 +15,21 @@ /** * Legacy variant of {@code WriteCrate}, written against the released 2.1.2 - * API. The example code is identical to the current variant; only the - * canonicalization helper uses the Jackson API. + * 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"); + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); public static void main(String[] args) throws Exception { Path workDir = Path.of(args[0]).getParent(); @@ -43,13 +45,14 @@ public static void main(String[] args) throws Exception { .save(crate, zip.toAbsolutePath().toString()); StringBuilder report = new StringBuilder(); - report.append("metadata\n") - .append(canonicalize(crate.getJsonMetadata())) - .append("\n\nfolder files (name: sha256)\n") - .append(folderFileDigests(folder)) - .append("\nzip entries (name: sha256)\n") - .append(zipEntryDigests(zip)) - .append("\n"); + 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()); } @@ -75,7 +78,8 @@ static String folderFileDigests(Path folder) throws Exception { static String zipEntryDigests(Path zip) throws Exception { StringBuilder report = new StringBuilder(); try (ZipFile zipFile = new ZipFile(zip.toFile())) { - List entries = zipFile.stream() + List entries = zipFile + .stream() .sorted(Comparator.comparing(ZipEntry::getName)) .toList(); for (ZipEntry entry : entries) { @@ -112,28 +116,4 @@ static String digest(InputStreamSupplier supplier) throws Exception { interface InputStreamSupplier { InputStream open() throws Exception; } - - /** - * Same canonicalization as the current variant, but with the legacy - * Jackson API: entities sorted by their {@code @id}, compact JSON. - */ - 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/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 index 05f1be8e..865010e2 100644 --- a/examples/src/main/java/examples/CreateMinimalCrate.java +++ b/examples/src/main/java/examples/CreateMinimalCrate.java @@ -3,19 +3,12 @@ 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.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.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** - * Builds a small crate programmatically and prints its metadata in - * canonical form (entities sorted by their {@code @id}, compact JSON). + * 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 @@ -48,33 +41,9 @@ public static void main(String[] args) throws Exception { ) .build(); - Path output = Path.of(args[0]); - Files.writeString(output, canonicalize(crate.getJsonMetadata())); - } - - /** - * Sorts the {@code @graph} entities by their {@code @id} (the entity order - * follows Java hash iteration and is unspecified) and writes compact JSON, - * so that runs of different library versions can be compared byte-for-byte. - */ - 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); + 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 index 8c1675db..8820ee8f 100644 --- a/examples/src/main/java/examples/ModifyCrate.java +++ b/examples/src/main/java/examples/ModifyCrate.java @@ -3,21 +3,15 @@ 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.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 java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** * Reads a fixture crate from disk and modifies it: adds and removes entities, - * adds and removes properties. Prints the canonical metadata after each step, - * so the comparison covers the mutation paths of the library. + * 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 { @@ -35,27 +29,40 @@ public static void main(String[] args) throws Exception { RoCrate crate = Readers.newFolderReader().readCrate(FIXTURE.toString()); StringBuilder report = new StringBuilder(); - report.append(canonicalize(crate.getJsonMetadata())).append("\n\n"); + report + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) + .append("\n\n"); - // remove the data entity by its id + // 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(canonicalize(crate.getJsonMetadata())) + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) .append("\n\n"); - // add it back, with different properties + // add it back with different properties crate.addDataEntity( new DataEntity.DataEntityBuilder() .setId("data.txt") .addType("File") - .addProperty("name", "Some data") + .addProperty("name", "Some other data") .addProperty("encodingFormat", "text/plain") .build() ); report - .append("after addDataEntity(data.txt)\n") - .append(canonicalize(crate.getJsonMetadata())) + .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 @@ -63,7 +70,7 @@ public static void main(String[] args) throws Exception { crate.getRootDataEntity().addProperty("keywords", "testing, parity"); report .append("after root data entity property changes\n") - .append(canonicalize(crate.getJsonMetadata())) + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) .append("\n\n"); // add and remove a contextual entity @@ -76,39 +83,13 @@ public static void main(String[] args) throws Exception { crate.addContextualEntity(organization); report .append("after addContextualEntity(organization)\n") - .append(canonicalize(crate.getJsonMetadata())) + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) .append("\n\n"); crate.deleteEntityById("https://ror.org/04jsmm180"); report .append("after deleteEntityById(organization)\n") - .append(canonicalize(crate.getJsonMetadata())); + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())); Files.writeString(Path.of(args[0]), report.toString()); } - - /** - * Sorts the {@code @graph} entities by their {@code @id} (the entity order - * follows Java hash iteration and is unspecified) and writes compact JSON, - * so that runs of different library versions can be compared byte-for-byte. - */ - 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/ReadCrate.java b/examples/src/main/java/examples/ReadCrate.java index 312cdffa..8976409f 100644 --- a/examples/src/main/java/examples/ReadCrate.java +++ b/examples/src/main/java/examples/ReadCrate.java @@ -1,71 +1,50 @@ package examples; import edu.kit.datamanager.ro_crate.RoCrate; -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.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; /** * 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 prints the - * canonical metadata for both paths. + * 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"); + "..", + "core", + "src", + "test", + "resources", + "crates", + "simple_crate" + ); public static void main(String[] args) throws Exception { - Path zip = Path.of(args[0]).getParent().resolve("simple_crate.zip"); - RoCrate folderCrate = Readers.newFolderReader().readCrate(FIXTURE.toString()); + 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()); + RoCrate zipCrate = Readers.newZipPathReader().readCrate( + zip.toAbsolutePath().toString() + ); StringBuilder report = new StringBuilder(); - report.append("from folder\n") - .append(canonicalize(folderCrate.getJsonMetadata())) - .append("\n\nfrom zip\n") - .append(canonicalize(zipCrate.getJsonMetadata())) - .append("\n"); - Files.writeString(Path.of(args[0]), report.toString()); - } - - /** - * Sorts the {@code @graph} entities by their {@code @id} (the entity order - * follows Java hash iteration and is unspecified) and writes compact JSON, - * so that runs of different library versions can be compared byte-for-byte. - */ - 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); + 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/WriteCrate.java b/examples/src/main/java/examples/WriteCrate.java index d3c71c90..36824d45 100644 --- a/examples/src/main/java/examples/WriteCrate.java +++ b/examples/src/main/java/examples/WriteCrate.java @@ -1,17 +1,12 @@ package examples; import edu.kit.datamanager.ro_crate.RoCrate; -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.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Stream; @@ -20,9 +15,10 @@ /** * Reads the fixture crate, writes it to a folder and to a zip archive, and - * reports the written files: the canonical metadata 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). + * 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 { @@ -52,7 +48,7 @@ public static void main(String[] args) throws Exception { StringBuilder report = new StringBuilder(); report .append("metadata\n") - .append(canonicalize(crate.getJsonMetadata())) + .append(CanonicalJson.canonicalize(crate.getJsonMetadata())) .append("\n\nfolder files (name: sha256)\n") .append(folderFileDigests(folder)) .append("\nzip entries (name: sha256)\n") @@ -121,30 +117,4 @@ static String digest(InputStreamSupplier supplier) throws Exception { interface InputStreamSupplier { InputStream open() throws Exception; } - - /** - * Sorts the {@code @graph} entities by their {@code @id} (the entity order - * follows Java hash iteration and is unspecified) and writes compact JSON, - * so that runs of different library versions can be compared byte-for-byte. - */ - 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/test/java/examples/ParityTest.java b/examples/src/test/java/examples/ParityTest.java index f0917d3c..7fbee032 100644 --- a/examples/src/test/java/examples/ParityTest.java +++ b/examples/src/test/java/examples/ParityTest.java @@ -6,22 +6,79 @@ 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: the released 2.1.2 artifact - * must produce byte-identical metadata to the current code with the - * jackson2 backend, which again must match the current code with the - * jackson3 backend. The run tasks (see build.gradle) execute each example - * once per variant in its own JVM and write to build/examples/<variant>/. + * 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 EXAMPLE = "CreateMinimalCrate"; + private static final String VARIANTS_PROPERTY = "rocrate.parity.variants"; + private static final String OUTPUT_SUFFIX = ".txt"; - private static String output(String variant) throws IOException { - Path file = EXAMPLES_DIR.resolve(variant).resolve(EXAMPLE + ".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" @@ -30,12 +87,29 @@ private static String output(String variant) throws IOException { } @Test - void legacyMatchesCurrentJackson2() throws IOException { - assertEquals(output("legacy"), output("current-jackson2")); + void everyVariantProducedTheSameExamples() throws IOException { + examples(); } @Test - void currentJackson2MatchesCurrentJackson3() throws IOException { - assertEquals(output("current-jackson2"), output("current-jackson3")); + 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 + + "'" + ); + } + } } } From cd88a58eaafbd466d94d322aedb189694389634a Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 23:31:58 +0200 Subject: [PATCH 19/21] fix(core): make backend discovery failures name the artifacts to add or exclude --- .../kit/datamanager/ro_crate/json/Json.java | 34 +++++-- .../ro_crate/validation/Validation.java | 33 +++++-- .../ro_crate/json/JsonDiscoveryTest.java | 96 +++++++++++++++++++ .../validation/ValidationDiscoveryTest.java | 55 +++++++++++ 4 files changed, 205 insertions(+), 13 deletions(-) create mode 100644 core/src/test/java/edu/kit/datamanager/ro_crate/json/JsonDiscoveryTest.java create mode 100644 core/src/test/java/edu/kit/datamanager/ro_crate/validation/ValidationDiscoveryTest.java 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 index 803405a1..02eabe6a 100644 --- 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 @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Iterator; import java.util.ServiceLoader; /** @@ -38,18 +39,39 @@ public static void setAdapter(JsonAdapter replacement) { } private static JsonAdapter discoverAdapter() { - var candidates = ServiceLoader.load(JsonAdapter.class).iterator(); + 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 JsonAdapter found on the classpath. Add the ro-crate-java " + - "jackson2 or jackson3 module as a dependency." + "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)." ); } - JsonAdapter discovered = candidates.next(); + T discovered = candidates.next(); if (candidates.hasNext()) { throw new IllegalStateException( - "Multiple JsonAdapters found on the classpath; ro-crate-java " + - "supports only one JSON backend at a time." + "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/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 index 8aee9631..2dca8024 100644 --- 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 @@ -1,6 +1,7 @@ package edu.kit.datamanager.ro_crate.validation; import edu.kit.datamanager.ro_crate.entities.validation.EntityValidationStrategy; +import java.util.Iterator; import java.util.ServiceLoader; /** @@ -43,19 +44,37 @@ public static EntityValidationStrategy entityStrategy() { } private static T discover(Class type) { - var candidates = ServiceLoader.load(type).iterator(); + 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 the " - + "ro-crate-java-jackson2 or ro-crate-java-jackson3 module as a " - + "dependency."); + "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."); + "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/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/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() + ); + } +} From 5c90ccc931813f41b10f1564a9107c6ce82f05e6 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Tue, 20 Oct 2026 23:31:58 +0200 Subject: [PATCH 20/21] docs: state that the backend choice is automatic for historical consumers --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cac53855..4b3aa09b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,9 @@ 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. +`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 From 096aaa57272c15080aa4ce81d68aca47176cddcc Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 22 Oct 2026 13:31:59 +0200 Subject: [PATCH 21/21] test(core): track the WorkflowHub fixture crate zip The WorkflowHubTest reads /crates/workflowhub/workflow-109-5.crate.zip from the test classpath, but the file was never tracked: the global '*.zip' ignore rule swallowed it, so it only survived as a stale copy in bin/test from before the module split. Any fresh checkout (CI, devcontainer) fails the test with a NullPointerException from the missing resource. Add a negation for test fixture crates under core/src/test/resources and commit the zip from that leftover copy. Verified in the devcontainer: ./gradlew :core:test --tests '*WorkflowHubTest*' passes. --- .gitignore | 2 ++ .../crates/workflowhub/workflow-109-5.crate.zip | Bin 0 -> 41737 bytes 2 files changed, 2 insertions(+) create mode 100644 core/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip 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/core/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip b/core/src/test/resources/crates/workflowhub/workflow-109-5.crate.zip new file mode 100644 index 0000000000000000000000000000000000000000..6f33ca7ea9cc60c55b2e94f6a25f3140f8b4dff4 GIT binary patch literal 41737 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 literal 0 HcmV?d00001