Skip to content

Commit 7f494e0

Browse files
milanmajchrakclaude
andcommitted
Publish a download_url that actually downloads
XOAI's ItemUtils builds a bitstream URL as baseUrl + "/bitstream/" + handle + "/" + sid + "/" + name, which is the DSpace 6 UI path. Under DSpace 7 the Angular application answers there, so an anonymous GET returns the app shell rather than the file - measured on an OPEN record, 446 KB of text/html instead of the 37-byte file. Every one of the 9125 distributions carried it, so a harvester following download_url got a web page. DSpace 7 serves the bytes at {server}/api/core/bitstreams/{uuid}/content, which is what the CMDI crosswalk already does: it ignores the XOAI field and builds its own URL from dspace.server.url. fn:getProperty is a Saxon extension the OAI runtime registers, but AbstractXSLTest builds a bare Saxon factory with no extensions, so calling it here would break every unit test. The same value is derivable from what XOAI already puts in the document - repository/@url + "/server" - and every bitstream carries its uuid. A bitstream without one keeps the url XOAI supplied, which is better than no url. Verified against the live repository: 15 download_url values taken at random from the output all return the file with its real content type, none returns HTML. access_url is unchanged and stays the landing page, which is what its own scope note asks for. The deeper fix belongs in ItemUtils, where it would correct the URL for every crosswalk at once. Also in this commit, from checking that the access level is right rather than merely consistent: 121 bitstreams were fetched anonymously from the REST content endpoint, stratified over the cells where the licence gate and others/access-status disagree. The licence gate matched what an unauthenticated client actually receives on 121 of 121; access-status matched on 41. Every record where they differ resolved in the licence gate's favour, so access_rights is left as it is - it is not a proxy for authorisation, it tracks it exactly, while access-status does not because DSpace computes it from the primary bitstream alone. Measured after the change over all 2628 records: 2628 valid, every counter zero, 97 tests and 187 assertions green, 69 of 70 stylesheet mutants killed, 23 portability inputs clean. The surviving mutant edits a FormatDate branch its callers make unreachable by pre-filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f97dada commit 7f494e0

3 files changed

Lines changed: 45 additions & 4 deletions

File tree

dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/CcmmXslTest.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,29 @@ public void ccmmFordSubjectKeepsTheSchemaElementOrder() throws Exception {
865865
equalTo("0"))));
866866
}
867867

868+
@Test
869+
public void ccmmDownloadUrlIsTheEndpointThatServesTheBytes() throws Exception {
870+
// XOAI builds the bitstream url as the DSpace 6 UI path; under DSpace 7 that path is
871+
// answered by the Angular application and returns HTML, so it is not a download link
872+
String result = apply("ccmm.xsl").to(resource(MAIN));
873+
assertThat(result, is(ccmm().withXPath(
874+
"//ccmm:distribution_downloadable_file/ccmm:download_url/ccmm:iri",
875+
equalTo("https://lindat.mff.cuni.cz/repository/server/api/core/bitstreams/"
876+
+ "18341be0-03ad-4a22-a1c8-4cd293c3f5da/content"))));
877+
assertThat(result, is(ccmm().withXPath(
878+
"count(//ccmm:download_url/ccmm:iri[contains(., '/bitstream/')])", equalTo("0"))));
879+
}
880+
881+
@Test
882+
public void ccmmBitstreamWithoutAUuidKeepsTheUrlXoaiSupplied() throws Exception {
883+
// nothing to build a REST URL from, so the source url is better than no url
884+
String result = apply("ccmm.xsl").to(resource(TOOL));
885+
assertThat(result, is(ccmm().withXPath(
886+
"//ccmm:distribution_downloadable_file[ccmm:title='morphodita.zip']"
887+
+ "/ccmm:download_url/ccmm:iri",
888+
equalTo("https://lindat.mff.cuni.cz/repository/bitstream/1/10/morphodita.zip"))));
889+
}
890+
868891
@Test
869892
public void ccmmDatasetCarriesItsOwnResolvableIri() throws Exception {
870893
// dataset/iri is optional in the XSD but it is the only resolvable identity the
@@ -1044,11 +1067,10 @@ public void ccmmAccessUrlIsTheLandingPageAndDownloadUrlIsTheFile() throws Except
10441067
assertThat(result, is(ccmm().withXPath(
10451068
"//ccmm:distribution_downloadable_file/ccmm:access_url/ccmm:iri",
10461069
equalTo("http://hdl.handle.net/11234/1-5678"))));
1047-
assertThat(result, is(ccmm().withXPath(
1048-
"//ccmm:distribution_downloadable_file/ccmm:download_url/ccmm:iri",
1049-
equalTo("https://lindat.mff.cuni.cz/repository/bitstream/11234/1-5678/1/corpus.txt"))));
10501070
assertThat(result, is(ccmm().withXPath(
10511071
"count(//ccmm:access_url/ccmm:iri[contains(., '/bitstream/')])", equalTo("0"))));
1072+
assertThat(result, is(ccmm().withXPath(
1073+
"count(//ccmm:access_url/ccmm:iri[. = //ccmm:download_url/ccmm:iri])", equalTo("0"))));
10521074
}
10531075

10541076
@Test

dspace-oai/src/test/resources/xoai-ccmm-test.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@
204204
<field name="url">https://lindat.mff.cuni.cz/repository/bitstream/11234/1-5678/1/corpus.txt</field>
205205
<field name="checksum">ee1c4e448a8f9f838df348dfee1fc11f</field>
206206
<field name="checksumAlgorithm">MD5</field>
207+
<field name="sid">1</field>
208+
<field name="id">18341be0-03ad-4a22-a1c8-4cd293c3f5da</field>
207209
</element>
208210
</element>
209211
</element>

dspace/config/crosswalks/oai/metadataFormats/ccmm.xsl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,18 @@
240240
then concat('http://hdl.handle.net/', normalize-space(string($handleField)))
241241
else ''"/>
242242

243+
<!--
244+
Where DSpace 7 actually serves bitstream bytes. The url XOAI puts on a bitstream is the
245+
DSpace 6 UI path (/bitstream/handle/sid/name); under DSpace 7 the Angular application
246+
answers there and returns HTML, so it is not a download link. The REST content endpoint
247+
is, and its base sits under the repository URL that XOAI already supplies.
248+
-->
249+
<xsl:variable name="repoUrlRaw"
250+
select="normalize-space((/doc:metadata/doc:element[@name='repository']/doc:field[@name='url'])[1])"/>
251+
<xsl:variable name="restBase"
252+
select="if ($repoUrlRaw = '') then ''
253+
else concat(replace($repoUrlRaw, '/+$', ''), '/server/api/core/bitstreams/')"/>
254+
243255
<xsl:variable name="accessionedAll"
244256
select="/doc:metadata/doc:element[@name='dc']/doc:element[@name='date']/doc:element[@name='accessioned']/doc:element/doc:field[@name='value']"/>
245257
<xsl:variable name="availableAll"
@@ -1775,6 +1787,11 @@
17751787
<xsl:variable name="size" select="normalize-space((doc:field[@name='size'])[1])"/>
17761788
<xsl:variable name="fmt" select="normalize-space(tokenize(normalize-space((doc:field[@name='format'])[1]), ';')[1])"/>
17771789
<xsl:variable name="nm" select="normalize-space((doc:field[@name='name'], doc:field[@name='originalName'])[normalize-space(.) != ''][1])"/>
1790+
<xsl:variable name="uuid" select="normalize-space((doc:field[@name='id'])[1])"/>
1791+
<!-- the endpoint that serves the bytes; the XOAI url is a UI path, not a download -->
1792+
<xsl:variable name="downloadUrl"
1793+
select="if ($restBase != '' and matches($uuid, '^[0-9a-fA-F-]{36}$'))
1794+
then concat($restBase, $uuid, '/content') else $url"/>
17781795
<xsl:variable name="sum" select="normalize-space((doc:field[@name='checksum'])[1])"/>
17791796
<xsl:variable name="alg" select="normalize-space((doc:field[@name='checksumAlgorithm'])[1])"/>
17801797
<xsl:if test="matches($url, '^https?://\S+$') and matches($size, '^[0-9]+$') and matches($fmt, '^[A-Za-z0-9!#$&amp;^_.+-]+/[A-Za-z0-9!#$&amp;^_.+-]+$')">
@@ -1789,7 +1806,7 @@
17891806
<ccmm:access_url>
17901807
<ccmm:iri><xsl:value-of select="if ($landingPage != '') then $landingPage else $url"/></ccmm:iri>
17911808
</ccmm:access_url>
1792-
<ccmm:download_url><ccmm:iri><xsl:value-of select="$url"/></ccmm:iri></ccmm:download_url>
1809+
<ccmm:download_url><ccmm:iri><xsl:value-of select="$downloadUrl"/></ccmm:iri></ccmm:download_url>
17931810
<xsl:variable name="eu" select="$euFileTypes/t[@m = $fmt]"/>
17941811
<ccmm:format>
17951812
<xsl:choose>

0 commit comments

Comments
 (0)