From 6d9f78ff7c43a05bda6aa8c1b417a50065e8e43f Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 27 Feb 2026 13:43:15 +0100 Subject: [PATCH 1/9] Add CCMM 1.1.0 OAI-PMH crosswalk (ccmm-xml metadataPrefix) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 140 ++++ .../src/test/resources/xoai-ccmm-test.xml | 107 +++ .../crosswalks/oai/metadataFormats/ccmm.xsl | 633 ++++++++++++++++++ dspace/config/crosswalks/oai/xoai.xml | 12 + 4 files changed, 892 insertions(+) create mode 100644 dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-test.xml create mode 100644 dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java new file mode 100644 index 000000000000..0ce9f43b141c --- /dev/null +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -0,0 +1,140 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.xoai.tests.stylesheets; + +import static org.dspace.xoai.tests.support.XmlMatcherBuilder.xml; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; + +import org.dspace.xoai.tests.support.XmlMatcherBuilder; +import org.junit.Test; + +/** + * Tests for the CCMM (Czech Common Metadata Model) 1.1.0 OAI-PMH crosswalk. + * + * @see CCMM Schema + * @see Issue #1145 + */ +public class CcmmXslTest extends AbstractXSLTest { + + private static final String CCMM_NS = "https://schema.ccmm.cz/research-data/1.1"; + + @Test + public void ccmmCanTransformInput() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Czech NLP Dataset v2.0")))); + } + + @Test + public void ccmmContainsPublicationYear() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:publication_year", equalTo("2025")))); + } + + @Test + public void ccmmContainsIdentifier() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier/ccmm:value", + equalTo("http://hdl.handle.net/11234/1-5678")))); + } + + @Test + public void ccmmContainsCreator() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:relation/ccmm:person/ccmm:name", + equalTo("Novak, Jan")))); + } + + @Test + public void ccmmContainsSubjects() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:subject[1]/ccmm:title", + equalTo("linguistics")))); + } + + @Test + public void ccmmContainsResourceType() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:resource_type/ccmm:label", + equalTo("corpus")))); + } + + @Test + public void ccmmContainsDescription() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:description/ccmm:description_text", + equalTo("A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol.")))); + } + + @Test + public void ccmmContainsLicense() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri", + equalTo("https://creativecommons.org/licenses/by/4.0/")))); + } + + @Test + public void ccmmContainsAccessRights() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_abf2")))); + } + + @Test + public void ccmmContainsPrimaryLanguage() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:primary_language/ccmm:label", + equalTo("ces")))); + } + + @Test + public void ccmmContainsAlternateTitle() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:alternate_title/ccmm:title", + equalTo("CND 2.0")))); + } + + @Test + public void ccmmContainsPublisher() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Distributor']/ccmm:relation/ccmm:organization/ccmm:name", + equalTo("Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics")))); + } + + @Test + public void ccmmContainsMetadataIdentification() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:conforms_to_standard/ccmm:iri", + equalTo("https://schema.ccmm.cz/research-data/1.1")))); + } + + @Test + public void ccmmCanTransformBasicXoaiInput() throws Exception { + // Test with the default xoai-test1.xml (simpler data) to ensure crosswalk + // handles missing fields gracefully + String result = apply("ccmm.xsl").to(resource("xoai-test1.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Test Webpage")))); + } + + private XmlMatcherBuilder ccmm() { + return xml() + .withNamespace("ccmm", CCMM_NS); + } +} diff --git a/dspace-oai/src/test/resources/xoai-ccmm-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-test.xml new file mode 100644 index 000000000000..96697ac43165 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-test.xml @@ -0,0 +1,107 @@ + + + + + + + + Novak, Jan + Svobodova, Marie + + + + + + + 2025-06-15T10:30:00Z + + + + + 2025-06-15T10:30:00Z + + + + + 2025-03-20 + + + + + + + http://hdl.handle.net/11234/1-5678 + + + + + + + A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol. + + + + + + + ces + + + + + + Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics + + + + + Creative Commons - Attribution 4.0 International + + + + https://creativecommons.org/licenses/by/4.0/ + + + + + CC BY 4.0 + + + + + + linguistics + Czech language + NLP + + + + + Czech NLP Dataset v2.0 + + + + CND 2.0 + + + + + + corpus + + + + + 11234/1-5678 + oai:lindat.mff.cuni.cz:11234/1-5678 + 2025-06-15 10:30:00.000 + + + LINDAT/CLARIAH-CZ + lindat-help@ufal.mff.cuni.cz + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl new file mode 100644 index 000000000000..d5b87214a962 --- /dev/null +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -0,0 +1,633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unknown Repository + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/DataCurator + DataCurator + + + + + https://schema.ccmm.cz/research-data/1.1 + CCMM 1.1 + + + + + + + + + + + + + + + + + + http://unknown.repository + + + + + + + + + + + + + + + + + + + + + + https://hdl.handle.net/ + Handle + + + + + + + + + + https://doi.org/ + DOI + + + + + + + + + + https://www.w3.org/ns/iana/uri-schemes + URI + + + + + + + + + + + + + https://hdl.handle.net/ + Handle + + + + + + + + + + + + + + + + + + + + + + + + + + + Untitled + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/Creator + Creator + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/Creator + Creator + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/Editor + Editor + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/Other + Other + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/contributorType/Distributor + Distributor + + + + + + + + + + + + + + + + + + 9999 + + + + + + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/dateType/Issued + Issued + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/dateType/Accepted + Accepted + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/dateType/Available + Available + + + + + + + + + 9999-01-01 + + + + https://model.ccmm.cz/vocabulary/datacite/dateType/Issued + Issued + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Dataset + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Software + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Text + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Image + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Collection + https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/coar/access_right/c_f1cf + embargoed access + + + http://purl.org/coar/access_right/c_16ec + restricted access + + + http://purl.org/coar/access_right/c_abf2 + open access + + + + + + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/ccmm/license/unspecified + + + + + + https://model.ccmm.cz/vocabulary/ccmm/license/unspecified + unspecified + + + + + + + + + + + + + + + + + + + + + + + + + + unspecified + + + + + + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/descriptionType/Abstract + Abstract + + + + + + + + + https://model.ccmm.cz/vocabulary/datacite/descriptionType/Abstract + Abstract + + + + + + + + + + + + + + + + + + + + + + European Commission + + + + + + + + + + + + + + + + + + + + + https://www.w3.org/ns/iana/uri-schemes + URI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index 723aa02d7311..cd502b454e7e 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -24,6 +24,7 @@ + This is the default context of the DSpace OAI-PMH data provider. @@ -102,6 +103,7 @@ + This context exports items following the openaire data archive rules. @@ -238,6 +240,16 @@ http://w3id.org/meta-share/meta-share/ ../Schema/ELG-SHARE.xsd + + + ccmm-xml + metadataFormats/ccmm.xsl + https://schema.ccmm.cz/research-data/1.1 + https://techlib.github.io/CCMM/dataset/schema.xsd + From c9b5b4ccfbd44f2a2b7a80a6cf278f6d47109613 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 27 Feb 2026 15:25:14 +0100 Subject: [PATCH 2/9] Apply Copilot review suggestions to CCMM crosswalk - Extract hardcoded fallback strings as XSLT variables at top of stylesheet (Unknown Repository, http://unknown.repository, Untitled, unspecified, 9999) - Add detailed comment explaining Handle URL extraction assumptions - Add comment explaining publisher to Distributor role mapping rationale - Fix time_reference fallback: use dc.date.accessioned instead of hardcoded 9999-01-01 when issued/available dates are missing - Improve FormatDate fallback: validate first 4 chars are digits before using as year, output empty ccmm:date element for invalid input - Improve identifier fallback logic: check if Handle-pattern URIs were actually found rather than just checking for raw element presence - Add 6 new fallback scenario tests (title, publication_year, subject, identifier, repository name, license) with dedicated minimal test fixture - All 21 tests pass (20 CCMM + 1 QDC) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 51 +++++++++++++++ .../test/resources/xoai-ccmm-minimal-test.xml | 25 ++++++++ .../crosswalks/oai/metadataFormats/ccmm.xsl | 62 ++++++++++++++----- 3 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-minimal-test.xml diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index 0ce9f43b141c..fb7834cef980 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -133,6 +133,57 @@ public void ccmmCanTransformBasicXoaiInput() throws Exception { assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Test Webpage")))); } + // ---- Fallback scenario tests ---- + + @Test + public void ccmmFallbackTitleIsUntitled() throws Exception { + // When dc.title is missing, fallback to "Untitled" + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:title", equalTo("Untitled")))); + } + + @Test + public void ccmmFallbackPublicationYearIs9999() throws Exception { + // When dc.date.issued and dc.date.accessioned are missing, fallback to "9999" + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:publication_year", equalTo("9999")))); + } + + @Test + public void ccmmFallbackSubjectIsUnspecified() throws Exception { + // When dc.subject is missing, fallback to "unspecified" + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:subject/ccmm:title", equalTo("unspecified")))); + } + + @Test + public void ccmmFallbackIdentifierUsesOthersHandle() throws Exception { + // When dc.identifier.uri and dc.identifier.doi are missing, use others/handle + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier/ccmm:value", + equalTo("http://hdl.handle.net/99999/test-1")))); + } + + @Test + public void ccmmFallbackRepositoryNameIsUnknown() throws Exception { + // When repository/name is missing, fallback to "Unknown Repository" + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:qualified_relation/ccmm:relation/ccmm:organization/ccmm:name", + equalTo("Unknown Repository")))); + } + + @Test + public void ccmmFallbackLicenseIsUnspecified() throws Exception { + // When dc.rights.uri is missing but dc.rights text exists, license IRI is unspecified + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri", + equalTo("https://model.ccmm.cz/vocabulary/ccmm/license/unspecified")))); + } + private XmlMatcherBuilder ccmm() { return xml() .withNamespace("ccmm", CCMM_NS); diff --git a/dspace-oai/src/test/resources/xoai-ccmm-minimal-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-minimal-test.xml new file mode 100644 index 000000000000..90f7a5496d4d --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-minimal-test.xml @@ -0,0 +1,25 @@ + + + + + + + All rights reserved + + + + + 99999/test-1 + oai:test.repository:99999/test-1 + 2025-01-01 00:00:00.000 + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl index d5b87214a962..0e1b8e8ec44c 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -18,6 +18,15 @@ + + + + + + + + + - Unknown Repository + @@ -109,7 +118,11 @@ - + @@ -119,7 +132,7 @@ - http://unknown.repository + @@ -169,8 +182,12 @@ - - + + @@ -205,7 +222,7 @@ - Untitled + @@ -284,7 +301,12 @@ - + @@ -312,7 +334,7 @@ - 9999 + @@ -369,12 +391,16 @@ - - + + + - 9999-01-01 + + + @@ -519,7 +545,7 @@ - unspecified + @@ -623,9 +649,17 @@ - + - + + + + + + + + + From 6d549e0e486d91c9514eae061fe8a393d35b6e3e Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:32:05 +0200 Subject: [PATCH 3/9] Fix CCMM crosswalk: transform errors, vocabularies, access rights, dates Verified against all 2628 live LINDAT records; every output validates against CCMM 1.1.0 in XSD 1.1 (0 invalid). - fix XPTY0004: doc:field[@name='value'] returns a sequence, and records with two language variants of a field passed it to concat()/contains(). 425 of 2628 records aborted; the resulting OAIException becomes HTTP 500 for the whole ListRecords page, so all 32 pages failed and nothing was harvestable - replace the model.ccmm.cz IRIs, which all return 404, with the registers CCMM 1.1.0 names in its own specification (en/dsv.ttl scope notes): COAR resource types and access rights, vocabs.ccmm.cz AgentRole / TimeReference / DescriptionType, EU Publications Office for languages - dc.publisher now maps to AgentRole/Publisher; Distributor does not exist at that level of the codelist - derive access_rights from others/access-status, which DSpace computes in DefaultAccessStatusHelper from the actual READ policies. The previous branches were dead (local.embargo.termslift occurs 0 times) or inverted (others/@restrictedAccess is set on 24 records, all of them open access). Correct for 746 of 2628 records before, 2628 of 2628 now - handle the CLARIN "0000" unknown-date convention and local.approximateDate.issued the way ClarinDateService does in the UI: the local field replaces dc.date.issued, a range becomes a time_interval and the original string is kept verbatim in ccmm:date_information - emit exactly one Issued time reference plus a Created one, as the CCMM profile requires; publication_year and the Issued year now always agree - stop emitting an invented licence IRI; fall back to others/cc and otherwise leave ccmm:license empty and move the wording to terms_of_use/description - the minimal test fixture used to produce schema-invalid output (no time_reference at all); it no longer can Co-Authored-By: Claude Opus 5 (1M context) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 114 ++++- .../xoai-ccmm-approximate-date-test.xml | 60 +++ .../src/test/resources/xoai-ccmm-test.xml | 5 + .../crosswalks/oai/metadataFormats/ccmm.xsl | 398 ++++++++++++------ 4 files changed, 440 insertions(+), 137 deletions(-) create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-approximate-date-test.xml diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index fb7834cef980..2ffbf45169bb 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -63,10 +63,14 @@ public void ccmmContainsSubjects() throws Exception { @Test public void ccmmContainsResourceType() throws Exception { + // META-SHARE "corpus" maps to the COAR resource type "dataset" String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:resource_type/ccmm:label", - equalTo("corpus")))); + "//ccmm:dataset/ccmm:resource_type/ccmm:iri", + equalTo("http://purl.org/coar/resource_type/c_ddb1")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:resource_type/ccmm:label[1]", + equalTo("dataset")))); } @Test @@ -95,10 +99,14 @@ public void ccmmContainsAccessRights() throws Exception { @Test public void ccmmContainsPrimaryLanguage() throws Exception { + // CCMM: "Use IRI identifier from the register + // http://publications.europa.eu/resource/authority/language" String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:primary_language/ccmm:label", - equalTo("ces")))); + "//ccmm:dataset/ccmm:primary_language/ccmm:iri", + equalTo("http://publications.europa.eu/resource/authority/language/CES")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:primary_language/ccmm:label)", equalTo("0")))); } @Test @@ -110,10 +118,10 @@ public void ccmmContainsAlternateTitle() throws Exception { } @Test - public void ccmmContainsPublisher() throws Exception { + public void ccmmPublisherUsesPublisherRole() throws Exception { String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Distributor']/ccmm:relation/ccmm:organization/ccmm:name", + "//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Publisher']/ccmm:relation/ccmm:organization/ccmm:name", equalTo("Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics")))); } @@ -176,12 +184,98 @@ public void ccmmFallbackRepositoryNameIsUnknown() throws Exception { } @Test - public void ccmmFallbackLicenseIsUnspecified() throws Exception { - // When dc.rights.uri is missing but dc.rights text exists, license IRI is unspecified + public void ccmmFallbackLicenseIsEmptyAndRightsTextBecomesDescription() throws Exception { + // When no licence URI is known, ccmm:license stays empty (which is valid) instead of + // carrying a made-up IRI; the free-text wording moves to terms_of_use/description. String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri", - equalTo("https://model.ccmm.cz/vocabulary/ccmm/license/unspecified")))); + "count(//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri)", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:description", + equalTo("All rights reserved")))); + } + + // ---- Controlled vocabularies ---- + + @Test + public void ccmmCreatorRoleUsesCcmmAgentRoleCodelist() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:role/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator")))); + } + + @Test + public void ccmmDateTypeUsesCcmmTimeReferenceCodelist() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:time_reference[1]/ccmm:date_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued")))); + } + + // ---- Access rights come from DSpace's own computed access status ---- + + @Test + public void ccmmAccessRightsFollowAccessStatus() throws Exception { + // others/access-status = open.access + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_abf2")))); + } + + @Test + public void ccmmAccessRightsFallBackToRestricted() throws Exception { + // No others/access-status at all: never claim open access + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_16ec")))); + } + + // ---- CLARIN approximate dates (dc.date.issued = "0000") ---- + + @Test + public void ccmmApproximateDateSetsPublicationYearToEarliestAttestedYear() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:publication_year", equalTo("1930")))); + } + + @Test + public void ccmmApproximateDateRangeBecomesTimeInterval() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" + + "/ccmm:beginning/ccmm:date", equalTo("1930-01-01")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" + + "/ccmm:end/ccmm:date", equalTo("1950-12-31")))); + } + + @Test + public void ccmmApproximateDateKeepsOriginalTextVerbatim() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:date_information", equalTo("cca 1930-1950")))); + } + + @Test + public void ccmmEmitsExactlyOneIssuedAndOneCreatedTimeReference() throws Exception { + // CCMM requires a Created time reference, and requires publication_year to equal the + // year of the Issued date - which several Issued references could not satisfy. + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Issued'])", equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Created'])", equalTo("1")))); + } + + @Test + public void ccmmApproximateDateNeverEmitsYearZero() throws Exception { + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_instant[starts-with(ccmm:date, '0000')])", equalTo("0")))); } private XmlMatcherBuilder ccmm() { diff --git a/dspace-oai/src/test/resources/xoai-ccmm-approximate-date-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-approximate-date-test.xml new file mode 100644 index 000000000000..075fb17009ef --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-approximate-date-test.xml @@ -0,0 +1,60 @@ + + + + + + + Film professionals 1 + + + + + + http://hdl.handle.net/20.500.12801/3901034-01 + + + + + + + 0000 + + + + + 2021-03-08T12:00:00Z + + + + + + oral history + + + + + + + + cca 1930-1950 + + + + + + 20.500.12801/3901034-01 + oai:lindat.mff.cuni.cz:20.500.12801/3901034-01 + 2025-01-01 00:00:00.000 + + restricted + + + + LINDAT/CLARIAH-CZ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-test.xml index 96697ac43165..ea93cc71341a 100644 --- a/dspace-oai/src/test/resources/xoai-ccmm-test.xml +++ b/dspace-oai/src/test/resources/xoai-ccmm-test.xml @@ -99,6 +99,11 @@ 11234/1-5678 oai:lindat.mff.cuni.cz:11234/1-5678 2025-06-15 10:30:00.000 + + + open.access + LINDAT/CLARIAH-CZ diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl index 0e1b8e8ec44c..e912e9d741b9 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -27,6 +27,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - https://model.ccmm.cz/vocabulary/datacite/contributorType/DataCurator - DataCurator + https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/DataManager + Data Manager @@ -117,7 +171,7 @@ - + @@ -315,8 +368,8 @@ - https://model.ccmm.cz/vocabulary/datacite/contributorType/Distributor - Distributor + https://vocabs.ccmm.cz/registry/codelist/AgentRole/Publisher + Publisher @@ -326,41 +379,90 @@ - + + + + + + + + + - - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + - + + + + + - + - - - - - - - - - - - - https://model.ccmm.cz/vocabulary/datacite/dateType/Issued - Issued - - - + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued + Issued + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Created + Created + + - + @@ -370,13 +472,13 @@ - https://model.ccmm.cz/vocabulary/datacite/dateType/Accepted + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Accepted Accepted - + @@ -386,49 +488,75 @@ - https://model.ccmm.cz/vocabulary/datacite/dateType/Available + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Available Available - - - - - - - - - - - - - https://model.ccmm.cz/vocabulary/datacite/dateType/Issued - Issued - - - - - + + + - - - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Dataset - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Software - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Text - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Image - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Collection - https://model.ccmm.cz/vocabulary/datacite/resourceTypeGeneral/Other - - - + + + http://purl.org/coar/resource_type/c_ddb1 + dataset + + + http://purl.org/coar/resource_type/c_5ce6 + software + + + http://purl.org/coar/resource_type/c_12ce + video + + + http://purl.org/coar/resource_type/c_18cc + sound + + + http://purl.org/coar/resource_type/c_ecc8 + still image + + + http://purl.org/coar/resource_type/c_18cf + text + + + http://purl.org/coar/resource_type/c_18cw + musical notation + + + http://purl.org/coar/resource_type/c_86bc + bibliography + + + http://purl.org/coar/resource_type/c_e059 + learning object + + + http://purl.org/coar/resource_type/c_e9a0 + interactive resource + + + http://purl.org/coar/resource_type/c_1843 + other + + @@ -437,40 +565,35 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + - - - - + @@ -479,50 +602,71 @@ + + + + + + - - + + http://purl.org/coar/access_right/c_abf2 + open access + + http://purl.org/coar/access_right/c_f1cf - embargoed access + + embargoes access - - http://purl.org/coar/access_right/c_16ec - restricted access + + http://purl.org/coar/access_right/c_14cb + metadata only access - http://purl.org/coar/access_right/c_abf2 - open access + + http://purl.org/coar/access_right/c_16ec + restricted access - + - - - - - - - - + + + + - - - https://model.ccmm.cz/vocabulary/ccmm/license/unspecified - - - + + + + + - - https://model.ccmm.cz/vocabulary/ccmm/license/unspecified - unspecified - + + + @@ -559,7 +703,7 @@ - https://model.ccmm.cz/vocabulary/datacite/descriptionType/Abstract + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract Abstract @@ -569,7 +713,7 @@ - https://model.ccmm.cz/vocabulary/datacite/descriptionType/Abstract + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract Abstract From 3552e5f0899a574bf2b64f8d63b387aea0d1f20b Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:36:24 +0200 Subject: [PATCH 4/9] Fix source repository IRI and xml:lang in the CCMM crosswalk - original_repository/iri named the item itself on every one of the 2628 records: it was derived with substring-before(uri, '/handle/'), which does not match LINDAT's hdl.handle.net URIs, so the otherwise branch emitted the whole item URI. XOAI already carries repository/@url; use it. - xml:lang was hardcoded "en" on subjects, alternate titles, descriptions and the rights wording. XOAI nests every value inside an element named after its language qualifier, so take the tag from there and fall back to "en" only for the unqualified "none" wrapper. Both covered by new tests. Re-checked over all 2628 live LINDAT records: still 2628/2628 valid against CCMM 1.1.0 in XSD 1.1. Co-Authored-By: Claude Opus 5 (1M context) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 19 +++++++++ .../src/test/resources/xoai-ccmm-test.xml | 4 ++ .../crosswalks/oai/metadataFormats/ccmm.xsl | 40 +++++++++++++++---- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index 2ffbf45169bb..70b81c5cd493 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -233,6 +233,25 @@ public void ccmmAccessRightsFallBackToRestricted() throws Exception { equalTo("http://purl.org/coar/access_right/c_16ec")))); } + @Test + public void ccmmOriginalRepositoryIsTheRepositoryUrl() throws Exception { + // XOAI carries repository/@url; deriving it from the item URI names the item itself + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:metadata_identification/ccmm:original_repository/ccmm:iri", + equalTo("https://lindat.mff.cuni.cz/repository/")))); + } + + @Test + public void ccmmXmlLangFollowsTheXoaiLanguageWrapper() throws Exception { + // a value stored under must not be tagged as English + String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:title[.='korpus']/@xml:lang", equalTo("cs")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:title[.='linguistics']/@xml:lang", equalTo("en")))); + } + // ---- CLARIN approximate dates (dc.date.issued = "0000") ---- @Test diff --git a/dspace-oai/src/test/resources/xoai-ccmm-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-test.xml index ea93cc71341a..da30b3952555 100644 --- a/dspace-oai/src/test/resources/xoai-ccmm-test.xml +++ b/dspace-oai/src/test/resources/xoai-ccmm-test.xml @@ -78,6 +78,9 @@ Czech language NLP + + korpus + @@ -106,6 +109,7 @@ + https://lindat.mff.cuni.cz/repository/ LINDAT/CLARIAH-CZ lindat-help@ufal.mff.cuni.cz diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl index e912e9d741b9..43bc100f0404 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -170,6 +170,14 @@ + + + + - - - + @@ -385,6 +391,24 @@ + + + + + + + + + en + + + + - + - + @@ -701,7 +725,7 @@ - + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract Abstract @@ -711,7 +735,7 @@ - + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract Abstract From 93618c2fc81bd6d1f54dd67699c93b9a106f491f Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:33:01 +0200 Subject: [PATCH 5/9] Correct every false statement the CCMM crosswalk made, and read the fields it dropped An adversarial audit over all 2628 live LINDAT records found 30 defects. They fall into three groups, and the first is the one that matters: the feed was stating things the source does not support. False statements, now fixed: - access_rights was taken from others/access-status alone. DefaultAccessStatusHelper derives that from the primary bitstream's READ policies, which on a CLARIN repository does not say whether the resource can be downloaded: the gate is the licence category in dc.rights.label plus others/restrictedAccess. 1216 of 2628 records carried a wrong statement - 1161 anonymously downloadable datasets published as "restricted access", 55 login-gated ones as "open access". Verified against the live repository: 12/12 sampled "restricted" records return HTTP 206 to an anonymous ranged GET, 11/12 "open" ACA/RES records return 401. - Every date_type label was the register's concept id ("Issued") rather than its prefLabel ("Date Issued") - 10512 labels, none correct. - conforms_to_standard identified the profile by this stylesheet's own XML namespace under the label "CCMM 1.1", a string that appears nowhere in the CCMM release. - license/label carried the CLARIN access category (PUB/ACA/RES) instead of the licence name, which was demoted to a description. 1932 records. - Hierarchical vocabulary paths ("People::Masaryk ...") were published verbatim as subject titles; the leaf is now the title and the path a classification_code. - Agent type was hard-coded per source field, so film studios and ANVL placeholders were published as people. Names are now typed from the value, and placeholders produce no agent at all. - publication_year took min() over the approximate years even when the record stated an exact issue date; a comma enumeration ("1920, 1932") became a continuous interval; a "0000" issue date fell through to the ingest timestamp; Created ignored the creation year the record states. - RelationType/Replaces and IsReplacedBy do not exist in the register - the concepts are Obsoletes and IsObsoletedBy. Every emitted IRI and label was rechecked against the register. Fields that were being dropped, now read: the contact person and the repository contact, the owning collection, dc.identifier.other, dc.source.uri, local.demo.uri, every dc.relation qualifier, dc.format, dc.coverage.placeName, given/family name parts, language names, local.sponsor, local.additional.metadata, local.size.info, local.refbox.format and the metashare tree. Bitstreams of the ORIGINAL bundle now become distributions with format, size and checksum. Portability, none of which changed LINDAT output: ISO 639-1 and region-tagged language codes are accepted, the stock DSpace type list is mapped, dates are proved castable before being emitted, the funding guard names the whole grantAgreement prefix, and a repository with no url is no longer described as living at each item's own address. Tests: 70 methods over 9 fixtures, including a stock-DSpace record, every date shape DSpace stores, and the two CLARIN access shapes. Element order is asserted directly - the CCMM schema is an xs:sequence and validating against it would pull xml.xsd and GML over the network at build time - plus closed-set guards so no off-register string can come back. Mutation testing: 45 of 46 single-edit mutants are now caught, against 20 of 72 before. Re-run over all 2628 live records: 2628/2628 valid against CCMM 1.1.0 in XSD 1.1. Co-Authored-By: Claude Opus 5 (1M context) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 1063 +++++-- .../xoai-ccmm-academic-licence-test.xml | 55 + .../xoai-ccmm-clarin-access-test.xml | 56 + .../resources/xoai-ccmm-date-shapes-test.xml | 41 + .../xoai-ccmm-enumerated-date-test.xml | 47 + .../xoai-ccmm-no-identifier-test.xml | 28 + .../resources/xoai-ccmm-stock-dspace-test.xml | 63 + .../src/test/resources/xoai-ccmm-test.xml | 334 ++- .../crosswalks/oai/metadataFormats/ccmm.xsl | 2642 +++++++++++------ 9 files changed, 3074 insertions(+), 1255 deletions(-) create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-clarin-access-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-date-shapes-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-enumerated-date-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-no-identifier-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-stock-dspace-test.xml diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index 70b81c5cd493..1e301c261970 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -1,304 +1,759 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.xoai.tests.stylesheets; - -import static org.dspace.xoai.tests.support.XmlMatcherBuilder.xml; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsEqual.equalTo; - -import org.dspace.xoai.tests.support.XmlMatcherBuilder; -import org.junit.Test; - -/** - * Tests for the CCMM (Czech Common Metadata Model) 1.1.0 OAI-PMH crosswalk. - * - * @see CCMM Schema - * @see Issue #1145 - */ -public class CcmmXslTest extends AbstractXSLTest { - - private static final String CCMM_NS = "https://schema.ccmm.cz/research-data/1.1"; - - @Test - public void ccmmCanTransformInput() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Czech NLP Dataset v2.0")))); - } - - @Test - public void ccmmContainsPublicationYear() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath("//ccmm:publication_year", equalTo("2025")))); - } - - @Test - public void ccmmContainsIdentifier() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:identifier/ccmm:value", - equalTo("http://hdl.handle.net/11234/1-5678")))); - } - - @Test - public void ccmmContainsCreator() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:relation/ccmm:person/ccmm:name", - equalTo("Novak, Jan")))); - } - - @Test - public void ccmmContainsSubjects() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:subject[1]/ccmm:title", - equalTo("linguistics")))); - } - - @Test - public void ccmmContainsResourceType() throws Exception { - // META-SHARE "corpus" maps to the COAR resource type "dataset" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:resource_type/ccmm:iri", - equalTo("http://purl.org/coar/resource_type/c_ddb1")))); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:resource_type/ccmm:label[1]", - equalTo("dataset")))); - } - - @Test - public void ccmmContainsDescription() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:description/ccmm:description_text", - equalTo("A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol.")))); - } - - @Test - public void ccmmContainsLicense() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri", - equalTo("https://creativecommons.org/licenses/by/4.0/")))); - } - - @Test - public void ccmmContainsAccessRights() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", - equalTo("http://purl.org/coar/access_right/c_abf2")))); - } - - @Test - public void ccmmContainsPrimaryLanguage() throws Exception { - // CCMM: "Use IRI identifier from the register - // http://publications.europa.eu/resource/authority/language" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:primary_language/ccmm:iri", - equalTo("http://publications.europa.eu/resource/authority/language/CES")))); - assertThat(result, is(ccmm().withXPath( - "count(//ccmm:dataset/ccmm:primary_language/ccmm:label)", equalTo("0")))); - } - - @Test - public void ccmmContainsAlternateTitle() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:alternate_title/ccmm:title", - equalTo("CND 2.0")))); - } - - @Test - public void ccmmPublisherUsesPublisherRole() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Publisher']/ccmm:relation/ccmm:organization/ccmm:name", - equalTo("Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics")))); - } - - @Test - public void ccmmContainsMetadataIdentification() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:metadata_identification/ccmm:conforms_to_standard/ccmm:iri", - equalTo("https://schema.ccmm.cz/research-data/1.1")))); - } - - @Test - public void ccmmCanTransformBasicXoaiInput() throws Exception { - // Test with the default xoai-test1.xml (simpler data) to ensure crosswalk - // handles missing fields gracefully - String result = apply("ccmm.xsl").to(resource("xoai-test1.xml")); - assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Test Webpage")))); - } - - // ---- Fallback scenario tests ---- - - @Test - public void ccmmFallbackTitleIsUntitled() throws Exception { - // When dc.title is missing, fallback to "Untitled" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:title", equalTo("Untitled")))); - } - - @Test - public void ccmmFallbackPublicationYearIs9999() throws Exception { - // When dc.date.issued and dc.date.accessioned are missing, fallback to "9999" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:publication_year", equalTo("9999")))); - } - - @Test - public void ccmmFallbackSubjectIsUnspecified() throws Exception { - // When dc.subject is missing, fallback to "unspecified" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:subject/ccmm:title", equalTo("unspecified")))); - } - - @Test - public void ccmmFallbackIdentifierUsesOthersHandle() throws Exception { - // When dc.identifier.uri and dc.identifier.doi are missing, use others/handle - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:identifier/ccmm:value", - equalTo("http://hdl.handle.net/99999/test-1")))); - } - - @Test - public void ccmmFallbackRepositoryNameIsUnknown() throws Exception { - // When repository/name is missing, fallback to "Unknown Repository" - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:metadata_identification/ccmm:qualified_relation/ccmm:relation/ccmm:organization/ccmm:name", - equalTo("Unknown Repository")))); - } - - @Test - public void ccmmFallbackLicenseIsEmptyAndRightsTextBecomesDescription() throws Exception { - // When no licence URI is known, ccmm:license stays empty (which is valid) instead of - // carrying a made-up IRI; the free-text wording moves to terms_of_use/description. - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath( - "count(//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri)", equalTo("0")))); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:description", - equalTo("All rights reserved")))); - } - - // ---- Controlled vocabularies ---- - - @Test - public void ccmmCreatorRoleUsesCcmmAgentRoleCodelist() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:role/ccmm:iri", - equalTo("https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator")))); - } - - @Test - public void ccmmDateTypeUsesCcmmTimeReferenceCodelist() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:time_reference[1]/ccmm:date_type/ccmm:iri", - equalTo("https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued")))); - } - - // ---- Access rights come from DSpace's own computed access status ---- - - @Test - public void ccmmAccessRightsFollowAccessStatus() throws Exception { - // others/access-status = open.access - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", - equalTo("http://purl.org/coar/access_right/c_abf2")))); - } - - @Test - public void ccmmAccessRightsFallBackToRestricted() throws Exception { - // No others/access-status at all: never claim open access - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-minimal-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", - equalTo("http://purl.org/coar/access_right/c_16ec")))); - } - - @Test - public void ccmmOriginalRepositoryIsTheRepositoryUrl() throws Exception { - // XOAI carries repository/@url; deriving it from the item URI names the item itself - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:metadata_identification/ccmm:original_repository/ccmm:iri", - equalTo("https://lindat.mff.cuni.cz/repository/")))); - } - - @Test - public void ccmmXmlLangFollowsTheXoaiLanguageWrapper() throws Exception { - // a value stored under must not be tagged as English - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:subject/ccmm:title[.='korpus']/@xml:lang", equalTo("cs")))); - assertThat(result, is(ccmm().withXPath( - "//ccmm:subject/ccmm:title[.='linguistics']/@xml:lang", equalTo("en")))); - } - - // ---- CLARIN approximate dates (dc.date.issued = "0000") ---- - - @Test - public void ccmmApproximateDateSetsPublicationYearToEarliestAttestedYear() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:dataset/ccmm:publication_year", equalTo("1930")))); - } - - @Test - public void ccmmApproximateDateRangeBecomesTimeInterval() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" - + "/ccmm:beginning/ccmm:date", equalTo("1930-01-01")))); - assertThat(result, is(ccmm().withXPath( - "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" - + "/ccmm:end/ccmm:date", equalTo("1950-12-31")))); - } - - @Test - public void ccmmApproximateDateKeepsOriginalTextVerbatim() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); - assertThat(result, is(ccmm().withXPath( - "//ccmm:time_reference/ccmm:date_information", equalTo("cca 1930-1950")))); - } - - @Test - public void ccmmEmitsExactlyOneIssuedAndOneCreatedTimeReference() throws Exception { - // CCMM requires a Created time reference, and requires publication_year to equal the - // year of the Issued date - which several Issued references could not satisfy. - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-test.xml")); - assertThat(result, is(ccmm().withXPath( - "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Issued'])", equalTo("1")))); - assertThat(result, is(ccmm().withXPath( - "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Created'])", equalTo("1")))); - } - - @Test - public void ccmmApproximateDateNeverEmitsYearZero() throws Exception { - String result = apply("ccmm.xsl").to(resource("xoai-ccmm-approximate-date-test.xml")); - assertThat(result, is(ccmm().withXPath( - "count(//ccmm:time_instant[starts-with(ccmm:date, '0000')])", equalTo("0")))); - } - - private XmlMatcherBuilder ccmm() { - return xml() - .withNamespace("ccmm", CCMM_NS); - } -} +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.xoai.tests.stylesheets; + +import static org.dspace.xoai.tests.support.XmlMatcherBuilder.xml; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; + +import org.dspace.xoai.tests.support.XmlMatcherBuilder; +import org.junit.Test; + +/** + * Tests for the CCMM (Czech Common Metadata Model) 1.1.0 OAI-PMH crosswalk. + * + *

The CCMM schema is an xs:sequence, so element order is part of validity, and every + * controlled value has to come from the register CCMM names for it. Validating against the + * schema itself would pull xml.xsd and GML over the network at build time, so the invariants + * that matter are asserted here instead: the order of the dataset children, and closed-set + * guards that fail if any off-register string or unresolvable IRI reappears.

+ * + * @see CCMM Schema + * @see Issue #1145 + */ +public class CcmmXslTest extends AbstractXSLTest { + + private static final String CCMM_NS = "https://schema.ccmm.cz/research-data/1.1"; + private static final String MAIN = "xoai-ccmm-test.xml"; + private static final String MINIMAL = "xoai-ccmm-minimal-test.xml"; + private static final String APPROX = "xoai-ccmm-approximate-date-test.xml"; + private static final String STOCK = "xoai-ccmm-stock-dspace-test.xml"; + private static final String DATES = "xoai-ccmm-date-shapes-test.xml"; + private static final String CLARIN = "xoai-ccmm-clarin-access-test.xml"; + private static final String ENUM = "xoai-ccmm-enumerated-date-test.xml"; + private static final String ACA = "xoai-ccmm-academic-licence-test.xml"; + private static final String BARE = "xoai-ccmm-no-identifier-test.xml"; + + @Test + public void ccmmCanTransformInput() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Czech NLP Dataset v2.0")))); + } + + @Test + public void ccmmContainsPublicationYear() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath("//ccmm:publication_year", equalTo("2025")))); + } + + @Test + public void ccmmContainsIdentifier() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier[1]/ccmm:value", + equalTo("http://hdl.handle.net/11234/1-5678")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier[1]/ccmm:scheme/ccmm:label", equalTo("Handle")))); + } + + @Test + public void ccmmKeepsIdentifiersOtherThanTheItemUri() throws Exception { + // dc.identifier.other is an external identifier and must not be dropped + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:identifier[ccmm:value='ISLRN-123-456-789'])", equalTo("1")))); + } + + @Test + public void ccmmContainsCreator() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:relation/ccmm:person/ccmm:name", + equalTo("Novak, Jan")))); + } + + @Test + public void ccmmSplitsPersonNamesIntoGivenAndFamily() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:person[ccmm:name='Novak, Jan']/ccmm:family_name", equalTo("Novak")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:person[ccmm:name='Novak, Jan']/ccmm:given_name", equalTo("Jan")))); + } + + @Test + public void ccmmTypesCorporateCreditsAsOrganizationsNotPeople() throws Exception { + // a newsreel producer is not a natural person + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:qualified_relation[ccmm:relation/ccmm:organization/ccmm:name='Aktualita'])", + equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:person[ccmm:name='Aktualita'])", equalTo("0")))); + } + + @Test + public void ccmmDropsAnvlPlaceholderAgents() throws Exception { + // "(:unav) Unknown author" names nobody and must not become an agent + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:person[contains(ccmm:name, '(:un')])", equalTo("0")))); + } + + @Test + public void ccmmEmitsCreatorAndPublisherEvenWhenTheRecordNamesNeither() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:qualified_relation[ccmm:role/ccmm:label='Creator'])", equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Publisher'])", + equalTo("1")))); + } + + @Test + public void ccmmContainsSubjects() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:subject[1]/ccmm:title", equalTo("linguistics")))); + } + + @Test + public void ccmmHierarchicalSubjectKeepsThePathAsAClassificationCode() throws Exception { + // "People::Masaryk ..." is a path through a thesaurus, not a subject heading + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject[ccmm:classification_code]/ccmm:title", + equalTo("Masaryk Tomas Garrigue (1850-1937)")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:classification_code", + equalTo("People::Masaryk Tomas Garrigue (1850-1937)")))); + } + + @Test + public void ccmmContainsResourceType() throws Exception { + // META-SHARE "corpus" maps to the COAR resource type "dataset" + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:resource_type/ccmm:iri", + equalTo("http://purl.org/coar/resource_type/c_ddb1")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:resource_type/ccmm:label[1]", equalTo("dataset")))); + } + + @Test + public void ccmmResourceTypeUnderstandsTheStockDspaceTypeList() throws Exception { + String result = apply("ccmm.xsl").to(resource(STOCK)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:resource_type/ccmm:iri", + equalTo("http://purl.org/coar/resource_type/c_6501")))); + } + + @Test + public void ccmmContainsDescription() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:description[ccmm:description_type/ccmm:label='Abstract']/ccmm:description_text", + equalTo("A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol.")))); + } + + @Test + public void ccmmUnqualifiedDescriptionIsNotAssertedToBeAnAbstract() throws Exception { + // dc.description without a qualifier says nothing about what kind of description it is + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:description[ccmm:description_text='Built from newspaper text collected in 2024.']" + + "/ccmm:description_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Other")))); + } + + @Test + public void ccmmLicenseCarriesTheLicenceNameNotTheAccessCategory() throws Exception { + // dc.rights.label is PUB/ACA/RES, which is a download gate, not the name of a licence + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri", + equalTo("https://creativecommons.org/licenses/by/4.0/")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:label", + equalTo("Creative Commons - Attribution 4.0 International")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:license/ccmm:label[.='PUB' or .='ACA' or .='RES'])", equalTo("0")))); + } + + @Test + public void ccmmContainsAccessRights() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_abf2")))); + } + + @Test + public void ccmmAccessRightsFollowTheClarinLicenceGate() throws Exception { + // A PUB licence with files is downloadable by anyone, whatever DefaultAccessStatusHelper + // computed from the primary bitstream's policies. + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:label", + equalTo("open access")))); + } + + @Test + public void ccmmAnItemWithNoFilesIsMetadataOnlyNotRestricted() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_14cb")))); + } + + @Test + public void ccmmContainsPrimaryLanguage() throws Exception { + // CCMM: "Use IRI identifier from the register + // http://publications.europa.eu/resource/authority/language" + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:primary_language/ccmm:iri", + equalTo("http://publications.europa.eu/resource/authority/language/CES")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:primary_language/ccmm:label", equalTo("Czech")))); + } + + @Test + public void ccmmOtherLanguageKeepsItsNameAligned() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:other_language/ccmm:iri", + equalTo("http://publications.europa.eu/resource/authority/language/ENG")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:other_language/ccmm:label", equalTo("English")))); + } + + @Test + public void ccmmAcceptsIso6391AndRegionSubtags() throws Exception { + // stock DSpace stores "en_US"; the register keys on the uppercased 639-3 code + String result = apply("ccmm.xsl").to(resource(STOCK)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:primary_language/ccmm:iri", + equalTo("http://publications.europa.eu/resource/authority/language/ENG")))); + } + + @Test + public void ccmmContainsAlternateTitle() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:alternate_title/ccmm:title", equalTo("CND 2.0")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:alternate_title/ccmm:alternate_title_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/AlternateTitle/TranslatedTitle")))); + } + + @Test + public void ccmmPublisherUsesPublisherRole() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[ccmm:role/ccmm:label='Publisher']" + + "/ccmm:relation/ccmm:organization/ccmm:name", + equalTo("Charles University, Faculty of Mathematics and Physics, " + + "Institute of Formal and Applied Linguistics")))); + } + + @Test + public void ccmmContributorOtherKeepsItsOwnRole() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:qualified_relation[ccmm:role/ccmm:label='Other']/ccmm:role/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/Other")))); + } + + @Test + public void ccmmContainsMetadataIdentification() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:conforms_to_standard/ccmm:iri", + equalTo("https://schema.ccmm.cz/research-data/1.1.0")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:conforms_to_standard/ccmm:label", + equalTo("CCMM RD 1.1.0")))); + } + + @Test + public void ccmmMetadataRecordCarriesItsOwnOaiIdentifier() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:iri", + equalTo("oai:lindat.mff.cuni.cz:11234/1-5678")))); + } + + @Test + public void ccmmMetadataRecordHasADataManager() throws Exception { + // dsv.ttl: the relationship shall contain at least one agent with role "Data Manager" + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:metadata_identification/ccmm:qualified_relation/ccmm:role/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/DataManager")))); + } + + @Test + public void ccmmCanTransformBasicXoaiInput() throws Exception { + // the default xoai-test1.xml exercises the paths where almost every field is missing + String result = apply("ccmm.xsl").to(resource("xoai-test1.xml")); + assertThat(result, is(ccmm().withXPath("//ccmm:title", equalTo("Test Webpage")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:identifier)", equalTo("2")))); + // dc.identifier.issn names its own scheme; it used to be dropped entirely + assertThat(result, is(ccmm().withXPath( + "//ccmm:identifier[ccmm:value='123456789']/ccmm:scheme/ccmm:label", equalTo("ISSN")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:terms_of_use)", equalTo("1")))); + } + + // ---- Fallback scenario tests ---- + + @Test + public void ccmmFallbackTitleIsUntitled() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:title", equalTo("Untitled")))); + } + + @Test + public void ccmmFallbackPublicationYearIs9999() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath("//ccmm:dataset/ccmm:publication_year", equalTo("9999")))); + } + + @Test + public void ccmmFallbackSubjectIsUnspecified() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:subject/ccmm:title", equalTo("unspecified")))); + } + + @Test + public void ccmmFallbackIdentifierUsesOthersHandle() throws Exception { + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier/ccmm:value", + equalTo("http://hdl.handle.net/99999/test-1")))); + } + + @Test + public void ccmmFallbackRepositoryDeclaresItselfUnavailable() throws Exception { + // an absent repository name is stated as the ANVL ":unav", not invented + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:metadata_identification/ccmm:qualified_relation" + + "/ccmm:relation/ccmm:organization/ccmm:name", + equalTo(":unav")))); + } + + @Test + public void ccmmFallbackLicenseIsEmptyAndRightsTextBecomesTheLabel() throws Exception { + // With no licence URI, ccmm:license carries the wording as its label rather than a + // made-up IRI; license_document allows a label without one. + String result = apply("ccmm.xsl").to(resource(MINIMAL)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:iri)", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:terms_of_use/ccmm:license/ccmm:label", + equalTo("All rights reserved")))); + } + + @Test + public void ccmmOriginalRepositoryIsTheRepositoryUrl() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:metadata_identification/ccmm:original_repository/ccmm:iri", + equalTo("https://lindat.mff.cuni.cz/repository/")))); + } + + @Test + public void ccmmOriginalRepositoryIsNeverTheItemItself() throws Exception { + // without repository/url there is no repository to name, and a handle URL is not one + String result = apply("ccmm.xsl").to(resource(STOCK)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:metadata_identification/ccmm:original_repository/ccmm:iri", + equalTo("urn:x-ccmm:repository-unknown")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:original_repository[contains(ccmm:iri, '123/456')])", equalTo("0")))); + } + + @Test + public void ccmmXmlLangFollowsTheXoaiLanguageWrapper() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:title[.='korpus']/@xml:lang", equalTo("cs")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:title[.='Korpus']/@xml:lang", equalTo("de")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:subject/ccmm:title[.='linguistics']/@xml:lang", equalTo("en")))); + } + + // ---- Contact, distribution, relations ---- + + @Test + public void ccmmContainsANamedContactPoint() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:contact_point/ccmm:person/ccmm:name", equalTo("Rosa, Rudolf")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:contact_point/ccmm:person/ccmm:contact_point/ccmm:email", + equalTo("rosa@example.org")))); + } + + @Test + public void ccmmContainsTheRepositoryContactAsAnOrganization() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:contact_point/ccmm:organization/ccmm:contact_point/ccmm:email", + equalTo("lindat-help@ufal.mff.cuni.cz")))); + } + + @Test + public void ccmmBitstreamsBecomeDownloadableDistributions() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:distribution/ccmm:distribution_downloadable_file/ccmm:title", + equalTo("corpus.txt")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:distribution_downloadable_file/ccmm:byte_size", equalTo("2048")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:distribution_downloadable_file/ccmm:format/ccmm:label", equalTo("text/plain")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:distribution_downloadable_file/ccmm:checksum/ccmm:checksum_value", + equalTo("ee1c4e448a8f9f838df348dfee1fc11f")))); + } + + @Test + public void ccmmRelationQualifiersBecomeTypedRelatedResources() throws Exception { + // the register has no "Replaces"; superseding is Obsoletes + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:related_resource[ccmm:iri='http://hdl.handle.net/11234/1-1111']" + + "/ccmm:resource_relation_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/RelationType/Obsoletes")))); + } + + @Test + public void ccmmOwningCollectionBecomesAPartOfRelation() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:related_resource[ccmm:title='Test Collection']" + + "/ccmm:resource_relation_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/RelationType/IsPartOf")))); + } + + @Test + public void ccmmProjectPageAndDemoBecomeRelatedResources() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:related_resource[ccmm:resource_url='https://example.org/czech-nlp-dataset'])", + equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:related_resource[ccmm:resource_url='https://example.org/demo'])", + equalTo("1")))); + } + + @Test + public void ccmmContainsLocationForACoveredPlace() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath("//ccmm:location/ccmm:name", equalTo("Praha")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:location/ccmm:relation_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/LocationRelation/Refers")))); + } + + @Test + public void ccmmContainsValidationResult() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:validation_result/ccmm:label", equalTo("Validated by the repository")))); + } + + @Test + public void ccmmFundingReferenceSpellsOutTheFunderAndTheProgramme() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:funding_reference/ccmm:local_identifier", equalTo("731015")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:funding_reference/ccmm:funding_program", equalTo("H2020")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:funding_reference/ccmm:funder/ccmm:organization/ccmm:name", + equalTo("European Commission")))); + } + + @Test + public void ccmmNonGrantInfoUrisNeverInventAFunder() throws Exception { + // info:eu-repo/semantics/... is not a grant agreement + String result = apply("ccmm.xsl").to(resource(STOCK)); + assertThat(result, is(ccmm().withXPath("count(//ccmm:funding_reference)", equalTo("0")))); + } + + // ---- Controlled vocabularies ---- + + @Test + public void ccmmCreatorRoleUsesCcmmAgentRoleCodelist() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:qualified_relation[1]/ccmm:role/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator")))); + } + + @Test + public void ccmmDateTypeUsesTheRegistersOwnLabels() throws Exception { + // the register's English prefLabel is "Date Issued", not the concept id "Issued" + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:time_reference[1]/ccmm:date_type/ccmm:iri", + equalTo("https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:time_reference[1]/ccmm:date_type/ccmm:label[1]", + equalTo("Date Issued")))); + } + + @Test + public void ccmmEmitsNoOffRegisterVocabularyStrings() throws Exception { + // a closed-set guard: these are the strings and hosts that were wrong before, and no + // future edit may bring them back + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:date_type/ccmm:label[.='Issued' or .='Created'" + + " or .='Accepted' or .='Available'])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:iri[contains(., 'model.ccmm.cz')])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:iri[contains(., 'w3.org/ns/iana')])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:iri[contains(., 'RelationType/Replaces')" + + " or contains(., 'RelationType/IsReplacedBy')])", equalTo("0")))); + } + + // ---- Element order: the CCMM dataset is an xs:sequence ---- + + @Test + public void ccmmEmitsTheDatasetChildrenInSchemaOrder() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:identifier[preceding-sibling::ccmm:title])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:title[preceding-sibling::ccmm:publication_year])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:publication_year[preceding-sibling::ccmm:time_reference])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:resource_type[preceding-sibling::ccmm:terms_of_use])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:terms_of_use[preceding-sibling::ccmm:subject])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:subject[preceding-sibling::ccmm:location])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:related_resource[preceding-sibling::ccmm:distribution])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:distribution[preceding-sibling::ccmm:validation_result])", + equalTo("0")))); + } + + @Test + public void ccmmEmitsNestedElementsInSchemaOrder() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:person/ccmm:name[preceding-sibling::ccmm:given_name])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:subject/ccmm:title[preceding-sibling::ccmm:classification_code])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:terms_of_use/ccmm:access_rights[preceding-sibling::ccmm:license])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:terms_of_use/ccmm:license[preceding-sibling::ccmm:contact_point])", + equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference/ccmm:temporal_representation" + + "[preceding-sibling::ccmm:date_type])", equalTo("0")))); + } + + // ---- CLARIN approximate dates (dc.date.issued = "0000") ---- + + @Test + public void ccmmApproximateDateSetsPublicationYearToTheStartOfTheRange() throws Exception { + String result = apply("ccmm.xsl").to(resource(APPROX)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:publication_year", equalTo("1930")))); + } + + @Test + public void ccmmApproximateDateRangeBecomesTimeInterval() throws Exception { + String result = apply("ccmm.xsl").to(resource(APPROX)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" + + "/ccmm:beginning/ccmm:date", equalTo("1930-01-01")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:temporal_representation/ccmm:time_interval" + + "/ccmm:end/ccmm:date", equalTo("1950-12-31")))); + } + + @Test + public void ccmmApproximateDateKeepsOriginalTextVerbatim() throws Exception { + String result = apply("ccmm.xsl").to(resource(APPROX)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference/ccmm:date_information", equalTo("cca 1930-1950")))); + } + + @Test + public void ccmmEmitsExactlyOneIssuedAndOneCreatedTimeReference() throws Exception { + // CCMM requires a Created time reference, and requires publication_year to equal the + // year of the Issued date - which several Issued references could not satisfy. + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Issued'])", equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Created'])", equalTo("1")))); + } + + @Test + public void ccmmApproximateDateNeverEmitsYearZero() throws Exception { + String result = apply("ccmm.xsl").to(resource(APPROX)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_instant[starts-with(ccmm:date, '0000')])", equalTo("0")))); + } + + // ---- Dates that no format string can parse ---- + + @Test + public void ccmmEveryEmittedDateIsAValidXsDate() throws Exception { + String result = apply("ccmm.xsl").to(resource(DATES)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:date[string-length(normalize-space(.)) != 10])", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:date[normalize-space(.) = ''])", equalTo("0")))); + } + + @Test + public void ccmmUnparsableDateProducesNoTimeReferenceAtAll() throws Exception { + // "n.d." is one of two dc.date.available values; only the parsable one survives + String result = apply("ccmm.xsl").to(resource(DATES)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Available'])", + equalTo("1")))); + } + + @Test + public void ccmmYearOnlyAndYearMonthDatesArePadded() throws Exception { + String result = apply("ccmm.xsl").to(resource(DATES)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Issued']" + + "/ccmm:temporal_representation/ccmm:time_instant/ccmm:date", + equalTo("2014-01-01")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Accepted']" + + "/ccmm:temporal_representation/ccmm:time_instant/ccmm:date", + equalTo("2011-12-01")))); + } + + @Test + public void ccmmPublicationYearAlwaysMatchesTheIssuedReference() throws Exception { + // dsv.ttl requires the two to agree; the fixtures carry different years so this can fail + for (String fixture : new String[] { MAIN, MINIMAL, APPROX, STOCK, DATES }) { + String result = apply("ccmm.xsl").to(resource(fixture)); + assertThat(fixture, result, is(ccmm().withXPath( + "substring((//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Issued']" + + "//ccmm:date | //ccmm:time_reference[ccmm:date_type/ccmm:label='Date Issued']" + + "//ccmm:date_time)[1], 1, 4) = string(//ccmm:dataset/ccmm:publication_year)", + equalTo("true")))); + } + } + + @Test + public void ccmmPubLicenceWithFilesIsOpenEvenWhenAccessStatusSaysRestricted() throws Exception { + // 1161 live records are anonymously downloadable while access-status calls them restricted + String result = apply("ccmm.xsl").to(resource(CLARIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_abf2")))); + } + + @Test + public void ccmmEnumeratedYearsNeverBecomeAContinuousInterval() throws Exception { + // "1920, 1932" names two years; the source never claims the years between them + String result = apply("ccmm.xsl").to(resource(ENUM)); + assertThat(result, is(ccmm().withXPath("count(//ccmm:time_interval)", equalTo("0")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:publication_year", equalTo("1932")))); + } + + @Test + public void ccmmCreatedUsesTheStatedYearOfCreation() throws Exception { + // local.additional.metadata states field_year; the deposit year is not a creation date + String result = apply("ccmm.xsl").to(resource(ENUM)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Created']" + + "/ccmm:temporal_representation/ccmm:time_instant/ccmm:date", + equalTo("1787-01-01")))); + } + + @Test + public void ccmmTitleIsNeverAConcatenationOfSeveralValues() throws Exception { + String result = apply("ccmm.xsl").to(resource(STOCK)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:title", equalTo("A Stock DSpace Item")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:title)", equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:alternate_title/ccmm:title", + equalTo("Second Title Nobody Entered As One")))); + } + + @Test + public void ccmmUnparsableAccessionDateProducesNoTimeReference() throws Exception { + String result = apply("ccmm.xsl").to(resource(DATES)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Accepted'])", + equalTo("1")))); + } + + @Test + public void ccmmUriIdentifierSchemeUsesTheIanaRegistry() throws Exception { + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:related_resource[ccmm:resource_url='https://example.org/czech-nlp-dataset']" + + "/ccmm:identifier/ccmm:scheme/ccmm:iri", + equalTo("https://www.iana.org/assignments/uri-schemes")))); + } + + @Test + public void ccmmAcademicLicenceIsRestrictedEvenWhenAccessStatusSaysOpen() throws Exception { + // the CLARIN identity form gates the download; open.access on the bitstream does not + String result = apply("ccmm.xsl").to(resource(ACA)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_16ec")))); + } + + @Test + public void ccmmAlwaysEmitsAtLeastOneIdentifier() throws Exception { + // identifier is 1..unbounded and this record carries none of the usual sources + String result = apply("ccmm.xsl").to(resource(BARE)); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:dataset/ccmm:identifier)", equalTo("1")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier/ccmm:value", + equalTo("oai:repo.example.org:no-handle-1")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:dataset/ccmm:identifier/ccmm:scheme/ccmm:iri", + equalTo("https://www.iana.org/assignments/uri-schemes")))); + } + + private XmlMatcherBuilder ccmm() { + return xml() + .withNamespace("ccmm", CCMM_NS); + } +} diff --git a/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml new file mode 100644 index 000000000000..1ffe04b0f1b2 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml @@ -0,0 +1,55 @@ + + + + + + + + 2018-06-01 + + + + + + CLARIN ACA End-User Licence Agreement + + + + ACA + + + + + + An Academic Use Corpus + + + + + + ORIGINAL + + + corpus.zip + application/zip + 4096 + https://lindat.mff.cuni.cz/repository/bitstream/1/3/corpus.zip + + + + + + 11234/1-9 + oai:lindat.mff.cuni.cz:11234/1-9 + + open.access + + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-clarin-access-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-clarin-access-test.xml new file mode 100644 index 000000000000..ee4dbb85fec3 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-clarin-access-test.xml @@ -0,0 +1,56 @@ + + + + + + + + 2019-01-01 + + + + + + Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International + + + + PUB + + + + + + A Publicly Licensed Film + + + + + + ORIGINAL + + + film.mp4 + video/mp4 + 1048576 + https://lindat.mff.cuni.cz/repository/bitstream/1/2/film.mp4 + + + + + + 20.500.12801/1-2 + oai:lindat.mff.cuni.cz:20.500.12801/1-2 + + + restricted + + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-date-shapes-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-date-shapes-test.xml new file mode 100644 index 000000000000..a1d786843b13 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-date-shapes-test.xml @@ -0,0 +1,41 @@ + + + + + + + + 2014 + + + + + 2011-12 + no date recorded + + + + + 2008-12-01 + n.d. + + + + + + Date Shapes + + + + + 123/789 + oai:repo.example.org:123/789 + + + Example Repository + https://repo.example.org/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-enumerated-date-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-enumerated-date-test.xml new file mode 100644 index 000000000000..f9684814f8ec --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-enumerated-date-test.xml @@ -0,0 +1,47 @@ + + + + + + + + 0000 + + + + + + A Compiled Newsreel + + + + + + + + 1920, 1932 + + + + + + + Nid:2307 +Begin year of resource creation (field_year):1787 +Readily Available (field_resource_available):Yes + + + + + + 20.500.12801/3-4 + oai:lindat.mff.cuni.cz:20.500.12801/3-4 + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-no-identifier-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-no-identifier-test.xml new file mode 100644 index 000000000000..d010c0100c28 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-no-identifier-test.xml @@ -0,0 +1,28 @@ + + + + + + + A Record With No Identifier + + + + + + 2021-02-03 + + + + + + oai:repo.example.org:no-handle-1 + + + Example Repository + https://repo.example.org/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-stock-dspace-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-stock-dspace-test.xml new file mode 100644 index 000000000000..571b32c31124 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-stock-dspace-test.xml @@ -0,0 +1,63 @@ + + + + + + + + Smith, Alice + + + + + + + 2024-05-01 + + + + + + + http://hdl.handle.net/123/456 + + + + + + + en_US + + + + + + info:eu-repo/semantics/openAccess + + info:eu-repo/semantics/altIdentifier/doi/10.1234/xyz + + + + + A Stock DSpace Item + Second Title Nobody Entered As One + + + + + Article + + + + + 123/456 + oai:repo.example.org:123/456 + + + Example Repository + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-test.xml index da30b3952555..a4b78dab53c6 100644 --- a/dspace-oai/src/test/resources/xoai-ccmm-test.xml +++ b/dspace-oai/src/test/resources/xoai-ccmm-test.xml @@ -1,116 +1,218 @@ - - - - - - - - Novak, Jan - Svobodova, Marie - - - - - - - 2025-06-15T10:30:00Z - - - - - 2025-06-15T10:30:00Z - - - - - 2025-03-20 - - - - - - - http://hdl.handle.net/11234/1-5678 - - - - - - - A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol. - - - - - - - ces - - - - - - Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics - - - - - Creative Commons - Attribution 4.0 International - - - - https://creativecommons.org/licenses/by/4.0/ - - - - - CC BY 4.0 - - - - - - linguistics - Czech language - NLP - - - korpus - - - - - Czech NLP Dataset v2.0 - - - - CND 2.0 - - - - - - corpus - - - - - 11234/1-5678 - oai:lindat.mff.cuni.cz:11234/1-5678 - 2025-06-15 10:30:00.000 - - - open.access - - - - https://lindat.mff.cuni.cz/repository/ - LINDAT/CLARIAH-CZ - lindat-help@ufal.mff.cuni.cz - - + + + + + + + + Novak, Jan + Svobodova, Marie + + Aktualita + (:unav) Unknown author + + + + + Dvorak, Petr + + + + + + + Praha + + + + + + + 2025-06-15T10:30:00Z + + + + + 2025-06-15T10:30:00Z + + + + + 2025-03-20 + + + + + + text/plain + + + + + + http://hdl.handle.net/11234/1-5678 + + + + + ISLRN-123-456-789 + + + + + + + A sample dataset for testing CCMM crosswalk output in the OAI-PMH protocol. + + + + Built from newspaper text collected in 2024. + + + + + + ces + eng + + + + + + Charles University, Faculty of Mathematics and Physics, Institute of Formal and Applied Linguistics + + + + + info:eu-repo/grantAgreement/EC/H2020/731015/EU//ELEXIS + + + + http://hdl.handle.net/11234/1-1111 + + + + + + Creative Commons - Attribution 4.0 International + + + + https://creativecommons.org/licenses/by/4.0/ + + + + + + PUB + + + + + + + https://example.org/czech-nlp-dataset + + + + + + linguistics + Czech language + NLP + + People::Masaryk Tomas Garrigue (1850-1937) + + + korpus + + + Korpus + + + + + Czech NLP Dataset v2.0 + + + + CND 2.0 + + + + + + corpus + + + + + + + + Rudolf;Rosa;rosa@example.org;Charles University + + + + + + + Czech + English + + + + + + + https://example.org/demo + + + + + + + + + true + + + + + + + ORIGINAL + + + corpus.txt + corpus.txt + text/plain + 2048 + https://lindat.mff.cuni.cz/repository/bitstream/11234/1-5678/1/corpus.txt + ee1c4e448a8f9f838df348dfee1fc11f + MD5 + + + + + + 11234/1-5678 + oai:lindat.mff.cuni.cz:11234/1-5678 + Test Collection + 2025-06-15 10:30:00.000 + + + open.access + + + + https://lindat.mff.cuni.cz/repository/ + LINDAT/CLARIAH-CZ + lindat-help@ufal.mff.cuni.cz + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl index 43bc100f0404..730dcda0e76a 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -1,835 +1,1807 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/DataManager - Data Manager - - - - - https://schema.ccmm.cz/research-data/1.1 - CCMM 1.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://hdl.handle.net/ - Handle - - - - - - - - - - https://doi.org/ - DOI - - - - - - - - - - https://www.w3.org/ns/iana/uri-schemes - URI - - - - - - - - - - - - - https://hdl.handle.net/ - Handle - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator - Creator - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator - Creator - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/Editor - Editor - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/Other - Other - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/AgentRole/Publisher - Publisher - - - - - - - - - - - - - - - - - - - - - - - - en - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued - Issued - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/TimeReference/Created - Created - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/TimeReference/Accepted - Accepted - - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/TimeReference/Available - Available - - - - - - - - - - - - - - - - http://purl.org/coar/resource_type/c_ddb1 - dataset - - - http://purl.org/coar/resource_type/c_5ce6 - software - - - http://purl.org/coar/resource_type/c_12ce - video - - - http://purl.org/coar/resource_type/c_18cc - sound - - - http://purl.org/coar/resource_type/c_ecc8 - still image - - - http://purl.org/coar/resource_type/c_18cf - text - - - http://purl.org/coar/resource_type/c_18cw - musical notation - - - http://purl.org/coar/resource_type/c_86bc - bibliography - - - http://purl.org/coar/resource_type/c_e059 - learning object - - - http://purl.org/coar/resource_type/c_e9a0 - interactive resource - - - http://purl.org/coar/resource_type/c_1843 - other - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - http://purl.org/coar/access_right/c_abf2 - open access - - - http://purl.org/coar/access_right/c_f1cf - - embargoes access - - - http://purl.org/coar/access_right/c_14cb - metadata only access - - - - http://purl.org/coar/access_right/c_16ec - restricted access - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract - Abstract - - - - - - - - - https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract - Abstract - - - - - - - - - - - - - - - - - - - - - - European Commission - - - - - - - - - - - - - - - - - - - - - https://www.w3.org/ns/iana/uri-schemes - URI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + abkaframharaasmaze + bakbelbulbenbodbre + boscatchecosceschv + cymdandeudivelleng + epospaesteusfasfin + faofrafrygleglaglg + gujhauhebhinhrvhat + hunhyeindislitajpn + javkatkazkhmkankor + kurkirlatltzlaolit + lavmlgmrimkdmalmon + marmsamltmyanobnep + nldnnonorocioripan + polpusporronrussan + sndsinslkslvsomsqi + srpsweswatamteltgk + thatukturtatuigukr + urduzbviewolxhoyid + yorzhozul + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/AgentRole/Contributor/DataManager + Data Manager + + + + + + + + + + + + + https://schema.ccmm.cz/research-data/1.1.0 + CCMM RD 1.1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://hdl.handle.net/ + Handle + + + + + + + + + + https://doi.org/ + DOI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://hdl.handle.net/ + Handle + + + + + + + + + + https://www.iana.org/assignments/uri-schemes + URI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/AlternateTitle/TranslatedTitle + Translated Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :unkn + + https://vocabs.ccmm.cz/registry/codelist/AgentRole/Creator + Creator + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/AgentRole/Publisher + Publisher + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Issued + Date Issued + Datum vydání + + + + + + + Issue date unknown; the year of the repository ingest date is used instead. + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Created + Date Created + Datum vytvoření + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Accepted + Date Accepted + Datum přijetí + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/TimeReference/Available + Date Available + Datum zveřejnění + + + + + + + + + + + + + + + + + + http://purl.org/coar/resource_type/c_ddb1 + dataset + + + http://purl.org/coar/resource_type/c_5ce6 + software + + + http://purl.org/coar/resource_type/c_12ce + video + + + http://purl.org/coar/resource_type/c_18cc + sound + + + http://purl.org/coar/resource_type/c_ecc8 + still image + + + http://purl.org/coar/resource_type/c_18cf + text + + + http://purl.org/coar/resource_type/c_18cw + musical notation + + + http://purl.org/coar/resource_type/c_86bc + bibliography + + + http://purl.org/coar/resource_type/c_e059 + learning object + + + http://purl.org/coar/resource_type/c_e9a0 + interactive resource + + + + http://purl.org/coar/resource_type/c_6501 + journal article + + + http://purl.org/coar/resource_type/c_2f33 + book + + + http://purl.org/coar/resource_type/c_3248 + book part + + + http://purl.org/coar/resource_type/c_46ec + thesis + + + http://purl.org/coar/resource_type/c_db06 + doctoral thesis + + + http://purl.org/coar/resource_type/c_bdcc + master thesis + + + http://purl.org/coar/resource_type/c_7a1f + bachelor thesis + + + http://purl.org/coar/resource_type/c_816b + preprint + + + http://purl.org/coar/resource_type/c_18gh + technical report + + + http://purl.org/coar/resource_type/c_5794 + conference paper + + + http://purl.org/coar/resource_type/c_8544 + lecture + + + http://purl.org/coar/resource_type/c_15cd + patent + + + http://purl.org/coar/resource_type/c_1843 + other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/coar/access_right/c_16ec + restricted access + + + http://purl.org/coar/access_right/c_abf2 + open access + + + http://purl.org/coar/access_right/c_abf2 + open access + + + http://purl.org/coar/access_right/c_f1cf + + embargoes access + + + + http://purl.org/coar/access_right/c_14cb + metadata only access + + + + http://purl.org/coar/access_right/c_16ec + restricted access + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Abstract + Abstract + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Other + Other + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/TechnicalInfo + Technical Info + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Other + Other + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Other + Other + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/Other + Other + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/TechnicalInfo + Technical Info + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/TechnicalInfo + Technical Info + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/DescriptionType/TechnicalInfo + Technical Info + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/LocationRelation/Refers + Refers to the location + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + European Commission + Ministry of Education, Youth and Sports of the Czech Republic + Czech Science Foundation + Technology Agency of the Czech Republic + + + + + + + + + + + + + + + + + + + European Union + National funding + + :unkn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://vocabs.ccmm.cz/registry/codelist/RelationType/IsReferencedBy + is referenced by + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IsPartOf + HasPart + Obsoletes + IsObsoletedBy + IsReferencedBy + References + IsVersionOf + HasVersion + Requires + IsRequiredBy + IsDerivedFrom + IsVariantFormOf + IsOriginalFormOf + + + + + + is part of + has part + obsoletes + is obsoleted by + is referenced by + references + is version of + has version + requires + is required by + is derived from + is variant form of + is original form of + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + https://portal.issn.org/ + ISSN + + + https://www.isbn-international.org/ + ISBN + + + https://doi.org/ + DOI + + + https://hdl.handle.net/ + Handle + + + https://www.iana.org/assignments/uri-schemes + URI + + + + + + + + + + + Local identifier + + + + + From 928f8b618ce4edf540823c0583df06e146eb5b1e Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:20:34 +0200 Subject: [PATCH 6/9] Register the xml prefix so @xml:lang assertions compile XmlMatcherBuilder builds its NamespaceContext from a plain map with no built-in prefixes, so JAXP cannot resolve the reserved "xml" prefix and ccmmXmlLangFollowsTheXoaiLanguageWrapper failed with "Prefix must resolve to a namespace: xml". Register it alongside ccmm. Co-Authored-By: Claude Opus 5 (1M context) --- .../java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index 1e301c261970..7142986555e2 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -30,6 +30,7 @@ public class CcmmXslTest extends AbstractXSLTest { private static final String CCMM_NS = "https://schema.ccmm.cz/research-data/1.1"; + private static final String XML_NS = "http://www.w3.org/XML/1998/namespace"; private static final String MAIN = "xoai-ccmm-test.xml"; private static final String MINIMAL = "xoai-ccmm-minimal-test.xml"; private static final String APPROX = "xoai-ccmm-approximate-date-test.xml"; @@ -753,7 +754,10 @@ public void ccmmAlwaysEmitsAtLeastOneIdentifier() throws Exception { } private XmlMatcherBuilder ccmm() { + // XmlMatcherBuilder's NamespaceContext is a plain map, so the reserved "xml" prefix has + // to be registered explicitly or JAXP cannot compile an @xml:lang step. return xml() - .withNamespace("ccmm", CCMM_NS); + .withNamespace("ccmm", CCMM_NS) + .withNamespace("xml", XML_NS); } } From e0be57c31944bceab26159e6b91a3cebabd78d17 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:33:15 +0200 Subject: [PATCH 7/9] Cover the access_rights and FormatDate branches Copilot flagged as untested Copilot asked for coverage of the embargo/restricted access paths and of the FormatDate variations. Most of what it listed is already asserted, and one of its premises was wrong - the crosswalk never reads local.embargo.termslift, embargo comes from others/access-status. Three branches genuinely had no assertion, so they get one: - others/restrictedAccess=true on a PUB record with files must still be restricted access; the download gate outranks the licence category. 19 live records take that path. - others/access-status=embargo must reach the COAR embargo term, not the restricted fallback. - a dc.date value carrying a time must stay a ccmm:date_time; time_instant is a choice, and truncating it into ccmm:date would silently drop the time. 73 test methods over 11 fixtures. Co-Authored-By: Claude Opus 5 (1M context) --- .../xoai/tests/stylesheets/CcmmXslTest.java | 42 ++++++++++++++ .../test/resources/xoai-ccmm-embargo-test.xml | 45 +++++++++++++++ .../xoai-ccmm-restricted-flag-test.xml | 57 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-embargo-test.xml create mode 100644 dspace-oai/src/test/resources/xoai-ccmm-restricted-flag-test.xml diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java index 7142986555e2..871f3d72af9a 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java @@ -40,6 +40,8 @@ public class CcmmXslTest extends AbstractXSLTest { private static final String ENUM = "xoai-ccmm-enumerated-date-test.xml"; private static final String ACA = "xoai-ccmm-academic-licence-test.xml"; private static final String BARE = "xoai-ccmm-no-identifier-test.xml"; + private static final String FLAG = "xoai-ccmm-restricted-flag-test.xml"; + private static final String EMBARGO = "xoai-ccmm-embargo-test.xml"; @Test public void ccmmCanTransformInput() throws Exception { @@ -660,6 +662,21 @@ public void ccmmYearOnlyAndYearMonthDatesArePadded() throws Exception { equalTo("2011-12-01")))); } + @Test + public void ccmmDateTimeValuesKeepTheirTimeAndUseTheDateTimeElement() throws Exception { + // time_instant is a choice of date_time or date; truncating a timestamp into date + // would silently drop the time + String result = apply("ccmm.xsl").to(resource(MAIN)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Accepted']" + + "/ccmm:temporal_representation/ccmm:time_instant/ccmm:date_time", + equalTo("2025-06-15T10:30:00Z")))); + assertThat(result, is(ccmm().withXPath( + "count(//ccmm:time_reference[ccmm:date_type/ccmm:label='Date Accepted']" + + "/ccmm:temporal_representation/ccmm:time_instant/ccmm:date)", + equalTo("0")))); + } + @Test public void ccmmPublicationYearAlwaysMatchesTheIssuedReference() throws Exception { // dsv.ttl requires the two to agree; the fixtures carry different years so this can fail @@ -739,6 +756,31 @@ public void ccmmAcademicLicenceIsRestrictedEvenWhenAccessStatusSaysOpen() throws equalTo("http://purl.org/coar/access_right/c_16ec")))); } + @Test + public void ccmmRestrictedAccessFlagOutranksAPubLicence() throws Exception { + // ItemUtils sets others/restrictedAccess when a bitstream licence demands the identity + // form; that gate outranks the PUB licence category + String result = apply("ccmm.xsl").to(resource(FLAG)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_16ec")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:label", + equalTo("restricted access")))); + } + + @Test + public void ccmmEmbargoedAccessStatusMapsToTheCoarEmbargoTerm() throws Exception { + // with no CLARIN licence category present, others/access-status decides + String result = apply("ccmm.xsl").to(resource(EMBARGO)); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:iri", + equalTo("http://purl.org/coar/access_right/c_f1cf")))); + assertThat(result, is(ccmm().withXPath( + "//ccmm:terms_of_use/ccmm:access_rights/ccmm:label", + equalTo("embargoes access")))); + } + @Test public void ccmmAlwaysEmitsAtLeastOneIdentifier() throws Exception { // identifier is 1..unbounded and this record carries none of the usual sources diff --git a/dspace-oai/src/test/resources/xoai-ccmm-embargo-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-embargo-test.xml new file mode 100644 index 000000000000..78205d634022 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-embargo-test.xml @@ -0,0 +1,45 @@ + + + + + + + + 2023-09-01 + + + + + + An Embargoed Dataset + + + + + + ORIGINAL + + + embargoed.csv + text/csv + 512 + https://repo.example.org/bitstream/1/5/embargoed.csv + + + + + + 123/999 + oai:repo.example.org:123/999 + + embargo + + + + Example Repository + https://repo.example.org/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-restricted-flag-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-restricted-flag-test.xml new file mode 100644 index 000000000000..33f0ef426f42 --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-restricted-flag-test.xml @@ -0,0 +1,57 @@ + + + + + + + + 2020-04-01 + + + + + + Creative Commons - Attribution 4.0 International + + + + PUB + + + + + + A Gated Public Resource + + + + + + ORIGINAL + + + data.zip + application/zip + 8192 + https://lindat.mff.cuni.cz/repository/bitstream/1/4/data.zip + + + + + + 11234/1-11 + oai:lindat.mff.cuni.cz:11234/1-11 + true + + open.access + + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + From 23aca5e8d59bd8d2039b97d2c79108488d0ff532 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:39:34 +0200 Subject: [PATCH 8/9] Add the DSpace licence header to the CCMM test fixtures license-maven-plugin checks src/** and fails the dspace-oai build without it. The check runs in the verify phase, so it only surfaced once the unit tests stopped failing first. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/resources/xoai-ccmm-academic-licence-test.xml | 9 +++++++++ .../test/resources/xoai-ccmm-approximate-date-test.xml | 9 +++++++++ .../src/test/resources/xoai-ccmm-clarin-access-test.xml | 9 +++++++++ .../src/test/resources/xoai-ccmm-date-shapes-test.xml | 9 +++++++++ dspace-oai/src/test/resources/xoai-ccmm-embargo-test.xml | 9 +++++++++ .../test/resources/xoai-ccmm-enumerated-date-test.xml | 9 +++++++++ dspace-oai/src/test/resources/xoai-ccmm-minimal-test.xml | 9 +++++++++ .../src/test/resources/xoai-ccmm-no-identifier-test.xml | 9 +++++++++ .../test/resources/xoai-ccmm-restricted-flag-test.xml | 9 +++++++++ .../src/test/resources/xoai-ccmm-stock-dspace-test.xml | 9 +++++++++ dspace-oai/src/test/resources/xoai-ccmm-test.xml | 9 +++++++++ 11 files changed, 99 insertions(+) diff --git a/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml index 1ffe04b0f1b2..2f9d95f23c0c 100644 --- a/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml +++ b/dspace-oai/src/test/resources/xoai-ccmm-academic-licence-test.xml @@ -1,4 +1,13 @@ + + + + + + + + 1943-11-15 + + + + + + + PUB + + + + + + + Germany||Austria + + + + + + + http://hdl.handle.net/20.500.12801/3900007-01 + + + + + + akce charitativni + + + + + A Christmas Party in Lucerna for Poor Children + + + + + clip + + + + + + ORIGINAL + + + segment.mov + video/quicktime + 104857600 + https://lindat.mff.cuni.cz/repository/bitstream/1/9/segment.mov + + + + + + 20.500.12801/3900007-02 + oai:lindat.mff.cuni.cz:20.500.12801/3900007-02 + + open.access + + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + diff --git a/dspace-oai/src/test/resources/xoai-ccmm-tool-test.xml b/dspace-oai/src/test/resources/xoai-ccmm-tool-test.xml new file mode 100644 index 000000000000..d351e33a739a --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-ccmm-tool-test.xml @@ -0,0 +1,105 @@ + + + + + + + + + 2021-05-20 + + + + + + + https://doi.org/10.5555/morphodita + + + + + + + https://ufal.mff.cuni.cz/morphodita Project page + + + + + + tagger + + + tagger + + + + + MorphoDiTa Morphological Tagger + + + MorphoDiTa + + + + + toolService + + + + + + + + Jana;Strakova;Charles University + + + + + + + + + tool + + + + + + + ORIGINAL + + + morphodita.zip + application/zip + 4096 + https://lindat.mff.cuni.cz/repository/bitstream/1/10/morphodita.zip + + + models.tar.gz + application/x-gzip + 8192 + https://lindat.mff.cuni.cz/repository/bitstream/1/10/models.tar.gz + + + + + + 11234/1-2000 + oai:lindat.mff.cuni.cz:11234/1-2000 + + + LINDAT/CLARIAH-CZ + https://lindat.mff.cuni.cz/repository/ + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl index 730dcda0e76a..c3be8d8e1637 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl @@ -26,9 +26,71 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -163,6 +225,21 @@ + + + + + + + + + + 10201 + 60101 + + + + + + + + + @@ -345,32 +442,17 @@ - - - - https://hdl.handle.net/ - Handle - - + - - - - https://doi.org/ - DOI - - + - - - - + + - - - + + https://hdl.handle.net/ Handle @@ -456,10 +535,17 @@ - - + + + + @@ -468,6 +554,7 @@ and not(normalize-space(.) = /doc:metadata/doc:element[@name='dc']/doc:element[@name='title']/doc:element/doc:field[@name='value']/normalize-space(.))"> +
@@ -483,10 +570,23 @@
+ + + + https://vocabs.ccmm.cz/registry/codelist/AlternateTitle/AlternativeTitle + Alternative Title + + + + @@ -556,6 +656,10 @@
+
+ + + + - - - @@ -977,9 +1086,6 @@ - - - @@ -1013,8 +1119,8 @@ select="boolean(doc:metadata/doc:element[@name='bundles']/doc:element[@name='bundle'][doc:field[@name='name'] = 'ORIGINAL']/doc:element[@name='bitstreams']/doc:element[@name='bitstream'])"/> + select="if (matches($licenseUriRaw, '^https?://\S+$')) then $licenseUriRaw + else if (matches($ccUri, '^https?://\S+$')) then $ccUri else ''"/> @@ -1072,6 +1178,15 @@ + + + http://rightsstatements.org/vocab/UND/1.0/ + Copyright Undetermined + @@ -1137,7 +1252,9 @@ - + + @@ -1170,6 +1287,9 @@ + + + @@ -1196,23 +1316,42 @@ - - - - - - + + + + + + + + + + + + + + + + + + OECD FORD Subject Category (Frascati) + Oborové třídění OECD FORD (Frascati) + + @@ -1233,6 +1372,7 @@ + @@ -1342,6 +1482,10 @@ + + + + @@ -1354,6 +1498,8 @@ --> + + @@ -1363,6 +1509,7 @@ + @@ -1370,6 +1517,7 @@ + @@ -1461,6 +1613,7 @@ + @@ -1490,7 +1643,8 @@ - + + @@ -1513,6 +1667,10 @@ + + + + + - - - - + + @@ -1567,6 +1725,10 @@ IsDerivedFrom IsVariantFormOf IsOriginalFormOf + + Other @@ -1585,6 +1747,7 @@ is derived from is variant form of is original form of + other @@ -1614,20 +1777,46 @@ - + - + + + + + - - + + + + + + + + + + + - - - - + + + + + + + @@ -1647,14 +1836,14 @@ - + - + @@ -1676,7 +1865,7 @@ - + Documentation @@ -1728,6 +1917,21 @@ description element - and the deprecated language variants of a subject path. --> + + + + + + + + @@ -1761,6 +1965,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +